[Pkg-javascript-commits] [node-proper-lockfile] 01/06: Import Upstream version 2.0.0
Paolo Greppi
paolog-guest at moszumanska.debian.org
Mon Dec 12 09:02:09 UTC 2016
This is an automated email from the git hooks/post-receive script.
paolog-guest pushed a commit to branch master
in repository node-proper-lockfile.
commit 99296df51f9a6f6d415c397ac329a34d9f36b203
Author: Paolo Greppi <paolo.greppi at libpf.com>
Date: Mon Dec 12 08:04:12 2016 +0000
Import Upstream version 2.0.0
---
.editorconfig | 18 +
.eslintrc.json | 8 +
.gitignore | 7 +
.travis.yml | 5 +
LICENSE | 19 +
README.md | 210 +++++++++
index.js | 374 ++++++++++++++++
lib/syncFs.js | 40 ++
package.json | 51 +++
test/.eslintrc.json | 8 +
test/fixtures/crash.js | 16 +
test/fixtures/stress.js | 137 ++++++
test/fixtures/unref.js | 14 +
test/mocha.opts | 2 +
test/test.js | 1116 +++++++++++++++++++++++++++++++++++++++++++++++
15 files changed, 2025 insertions(+)
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..06369b1
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+root = 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
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..1258e02
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,8 @@
+{
+ "root": true,
+ "extends": [
+ "@satazor/eslint-config/es6",
+ "@satazor/eslint-config/addons/node",
+ "@satazor/eslint-config/addons/node-v4-es6"
+ ]
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..93fd06d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+/node_modules
+/npm-debug.*
+/test/tmp*
+/test/nonexistentfile*
+/test/*.log
+/test/coverage
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..3012f98
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js:
+ - "4"
+ - "6"
+script: "npm run test-travis"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..db5e914
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c2f4ea6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,210 @@
+# 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
+
+A 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) => {
+ 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(function (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', function (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).
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..6793fd0
--- /dev/null
+++ b/index.js
@@ -0,0 +1,374 @@
+'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
+ 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;
diff --git a/lib/syncFs.js b/lib/syncFs.js
new file mode 100644
index 0000000..218ea1d
--- /dev/null
+++ b/lib/syncFs.js
@@ -0,0 +1,40 @@
+'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;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b149a56
--- /dev/null
+++ b/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "proper-lockfile",
+ "version": "2.0.0",
+ "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 <hello at indigounited.com> (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"
+ }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
new file mode 100644
index 0000000..6ed962b
--- /dev/null
+++ b/test/.eslintrc.json
@@ -0,0 +1,8 @@
+{
+ "env": {
+ "mocha": true
+ },
+ "rules": {
+ "no-invalid-this": 0
+ }
+}
diff --git a/test/fixtures/crash.js b/test/fixtures/crash.js
new file mode 100644
index 0000000..5820cec
--- /dev/null
+++ b/test/fixtures/crash.js
@@ -0,0 +1,16 @@
+'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');
+});
diff --git a/test/fixtures/stress.js b/test/fixtures/stress.js
new file mode 100644
index 0000000..28daef3
--- /dev/null
+++ b/test/fixtures/stress.js
@@ -0,0 +1,137 @@
+'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();
+}
diff --git a/test/fixtures/unref.js b/test/fixtures/unref.js
new file mode 100644
index 0000000..0c015fb
--- /dev/null
+++ b/test/fixtures/unref.js
@@ -0,0 +1,14 @@
+'use strict';
+
+const fs = require('fs');
+const lockfile = require('../../');
+
+const file = `${__dirname}/../tmp`;
+
+fs.writeFileSync(file, '');
+
+lockfile.lock(file, (err) => {
+ if (err) {
+ throw err;
+ }
+});
diff --git a/test/mocha.opts b/test/mocha.opts
new file mode 100644
index 0000000..ceb81f8
--- /dev/null
+++ b/test/mocha.opts
@@ -0,0 +1,2 @@
+--timeout 10000
+--bail
diff --git a/test/test.js b/test/test.js
new file mode 100644
index 0000000..e1b9685
--- /dev/null
+++ b/test/test.js
@@ -0,0 +1,1116 @@
+'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) {
+ if (process.env.TRAVIS) {
+ process.stdout.write(stdout);
+ } else {
+ fs.writeFileSync(`${__dirname}/stress.log`, stdout || '');
+ }
+
+ return next(err);
+ }
+
+ next();
+ });
+ });
+});
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-proper-lockfile.git
More information about the Pkg-javascript-commits
mailing list