[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