├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── adapter.js ├── lockfile.js └── mtime-precision.js ├── package-lock.json ├── package.json └── test ├── check.test.js ├── fixtures ├── compromised.js ├── crash.js ├── stress.js └── unref.js ├── lock.test.js ├── misc.test.js ├── release.test.js ├── sync.test.js ├── unlock.test.js └── util └── unlockAll.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [package.json] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint-config-moxy/es9", 5 | "eslint-config-moxy/addons/node", 6 | "eslint-config-moxy/addons/jest" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.* 3 | coverage/ 4 | test/tmp/ 5 | test/*.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "lts/*" 5 | # Report coverage 6 | after_success: 7 | - "npm i codecov" 8 | - "node_modules/.bin/codecov" 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [4.1.2](https://github.com/moxystudio/node-proper-lockfile/compare/v4.1.1...v4.1.2) (2021-01-25) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * fix node 14 updating graceful-fs ([#102](https://github.com/moxystudio/node-proper-lockfile/issues/102)) ([b0d988e](https://github.com/moxystudio/node-proper-lockfile/commit/b0d988e)) 12 | 13 | 14 | 15 | 16 | ## [4.1.1](https://github.com/moxystudio/node-proper-lockfile/compare/v4.1.0...v4.1.1) (2019-04-03) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * fix mtime precision on some filesystems ([#88](https://github.com/moxystudio/node-proper-lockfile/issues/88)) ([f266158](https://github.com/moxystudio/node-proper-lockfile/commit/f266158)), closes [#82](https://github.com/moxystudio/node-proper-lockfile/issues/82) [#87](https://github.com/moxystudio/node-proper-lockfile/issues/87) 22 | 23 | 24 | 25 | 26 | # [4.1.0](https://github.com/moxystudio/node-proper-lockfile/compare/v4.0.0...v4.1.0) (2019-03-18) 27 | 28 | 29 | ### Features 30 | 31 | * allow second precision in mtime comparison ([#78](https://github.com/moxystudio/node-proper-lockfile/issues/78)) ([b2816a6](https://github.com/moxystudio/node-proper-lockfile/commit/b2816a6)) 32 | 33 | 34 | 35 | 36 | # [4.0.0](https://github.com/moxystudio/node-proper-lockfile/compare/v3.2.0...v4.0.0) (2019-03-12) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * fix typo in error message ([#68](https://github.com/moxystudio/node-proper-lockfile/issues/68)) ([b91cb55](https://github.com/moxystudio/node-proper-lockfile/commit/b91cb55)) 42 | 43 | 44 | ### Features 45 | 46 | * make staleness check more robust ([#74](https://github.com/moxystudio/node-proper-lockfile/issues/74)) ([9cc0973](https://github.com/moxystudio/node-proper-lockfile/commit/9cc0973)), closes [#71](https://github.com/moxystudio/node-proper-lockfile/issues/71) [/github.com/ipfs/js-ipfs-repo/issues/188#issuecomment-468682971](https://github.com//github.com/ipfs/js-ipfs-repo/issues/188/issues/issuecomment-468682971) 47 | 48 | 49 | ### BREAKING CHANGES 50 | 51 | * We were marking the lock as compromised when system went into sleep or if the event loop was busy taking too long to run the internals timers, Now we keep track of the mtime updated by the current process, and if we lose some cycles in the update process but recover and the mtime is still ours we do not mark the lock as compromised. 52 | 53 | 54 | 55 | 56 | # [3.2.0](https://github.com/moxystudio/node-proper-lockfile/compare/v3.1.0...v3.2.0) (2018-11-19) 57 | 58 | 59 | ### Features 60 | 61 | * add lock path option ([#66](https://github.com/moxystudio/node-proper-lockfile/issues/66)) ([32f1b8d](https://github.com/moxystudio/node-proper-lockfile/commit/32f1b8d)) 62 | 63 | 64 | 65 | 66 | # [3.1.0](https://github.com/moxystudio/node-proper-lockfile/compare/v3.0.2...v3.1.0) (2018-11-15) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * **package:** update retry to version 0.12.0 ([#50](https://github.com/moxystudio/node-proper-lockfile/issues/50)) ([d400b98](https://github.com/moxystudio/node-proper-lockfile/commit/d400b98)) 72 | 73 | 74 | ### Features 75 | 76 | * add signal exit ([#65](https://github.com/moxystudio/node-proper-lockfile/issues/65)) ([f20bc45](https://github.com/moxystudio/node-proper-lockfile/commit/f20bc45)) 77 | 78 | 79 | 80 | 81 | ## [3.0.2](https://github.com/moxystudio/node-proper-lockfile/compare/v3.0.1...v3.0.2) (2018-01-30) 82 | 83 | 84 | 85 | 86 | ## [3.0.1](https://github.com/moxystudio/node-proper-lockfile/compare/v3.0.0...v3.0.1) (2018-01-20) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * restore ability to use lockfile() directly ([0ef8fbc](https://github.com/moxystudio/node-proper-lockfile/commit/0ef8fbc)) 92 | 93 | 94 | 95 | 96 | # [3.0.0](https://github.com/moxystudio/node-proper-lockfile/compare/v2.0.1...v3.0.0) (2018-01-20) 97 | 98 | 99 | ### Chores 100 | 101 | * update project to latest node lts ([b1d43e5](https://github.com/moxystudio/node-proper-lockfile/commit/b1d43e5)) 102 | 103 | 104 | ### BREAKING CHANGES 105 | 106 | * remove callback support 107 | * use of node lts language features such as object spread 108 | * compromised function in lock() has been moved to an option 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Made With MOXY Lda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proper-lockfile 2 | 3 | [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Coverage Status][codecov-image]][codecov-url] [![Dependency status][david-dm-image]][david-dm-url] [![Dev Dependency status][david-dm-dev-image]][david-dm-dev-url] 4 | 5 | [npm-url]:https://npmjs.org/package/proper-lockfile 6 | [downloads-image]:https://img.shields.io/npm/dm/proper-lockfile.svg 7 | [npm-image]:https://img.shields.io/npm/v/proper-lockfile.svg 8 | [travis-url]:https://travis-ci.org/moxystudio/node-proper-lockfile 9 | [travis-image]:https://img.shields.io/travis/moxystudio/node-proper-lockfile/master.svg 10 | [codecov-url]:https://codecov.io/gh/moxystudio/node-proper-lockfile 11 | [codecov-image]:https://img.shields.io/codecov/c/github/moxystudio/node-proper-lockfile/master.svg 12 | [david-dm-url]:https://david-dm.org/moxystudio/node-proper-lockfile 13 | [david-dm-image]:https://img.shields.io/david/moxystudio/node-proper-lockfile.svg 14 | [david-dm-dev-url]:https://david-dm.org/moxystudio/node-proper-lockfile?type=dev 15 | [david-dm-dev-image]:https://img.shields.io/david/dev/moxystudio/node-proper-lockfile.svg 16 | 17 | An inter-process and inter-machine lockfile utility that works on a local or network file system. 18 | 19 | 20 | ## Installation 21 | 22 | `$ npm install proper-lockfile` 23 | 24 | 25 | ## Design 26 | 27 | There are various ways to achieve [file locking](http://en.wikipedia.org/wiki/File_locking). 28 | 29 | This library utilizes the `mkdir` strategy which works atomically on any kind of file system, even network based ones. 30 | The lockfile path is based on the file path you are trying to lock by suffixing it with `.lock`. 31 | 32 | 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`. 33 | 34 | 35 | ### Comparison 36 | 37 | This library is similar to [lockfile](https://github.com/isaacs/lockfile) but the latter has some drawbacks: 38 | 39 | - 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. 40 | 41 | > O_EXCL is broken on NFS file systems; programs which rely on it for performing locking tasks will contain a race condition. 42 | 43 | - 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. 44 | 45 | - It does not check if the lockfile was compromised which can lead to undesirable situations. `proper-lockfile` checks the lockfile when updating the `mtime`. 46 | 47 | - It has a default value of `0` for the stale option which isn't good because any crash or process kill that the package can't handle gracefully will leave the lock active forever. 48 | 49 | 50 | ### Compromised 51 | 52 | `proper-lockfile` does not detect cases in which: 53 | 54 | - A `lockfile` is manually removed and someone else acquires the lock right after 55 | - Different `stale`/`update` values are being used for the same file, possibly causing two locks to be acquired on the same file 56 | 57 | `proper-lockfile` detects cases in which: 58 | 59 | - Updates to the `lockfile` fail 60 | - Updates take longer than expected, possibly causing the lock to become stale for a certain amount of time 61 | 62 | 63 | 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. 64 | 65 | 66 | ## Usage 67 | 68 | ### .lock(file, [options]) 69 | 70 | Tries to acquire a lock on `file` or rejects the promise on error. 71 | 72 | If the lock succeeds, a `release` function is provided that should be called when you want to release the lock. The `release` function also rejects the promise on error (e.g. when the lock was already compromised). 73 | 74 | Available options: 75 | 76 | - `stale`: Duration in milliseconds in which the lock is considered stale, defaults to `10000` (minimum value is `5000`) 77 | - `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`) 78 | - `retries`: The number of retries or a [retry](https://www.npmjs.org/package/retry) options object, defaults to `0` 79 | - `realpath`: Resolve symlinks using realpath, defaults to `true` (note that if `true`, the `file` must exist previously) 80 | - `fs`: A custom fs to use, defaults to `graceful-fs` 81 | - `onCompromised`: Called if the lock gets compromised, defaults to a function that simply throws the error which will probably cause the process to die 82 | - `lockfilePath`: Custom lockfile path. e.g.: If you want to lock a directory and create the lock file inside it, you can pass `file` as `` and `options.lockfilePath` as `/dir.lock` 83 | 84 | 85 | ```js 86 | const lockfile = require('proper-lockfile'); 87 | 88 | lockfile.lock('some/file') 89 | .then((release) => { 90 | // Do something while the file is locked 91 | 92 | // Call the provided release function when you're done, 93 | // which will also return a promise 94 | return release(); 95 | }) 96 | .catch((e) => { 97 | // either lock could not be acquired 98 | // or releasing it failed 99 | console.error(e) 100 | }); 101 | 102 | // Alternatively, you may use lockfile('some/file') directly. 103 | ``` 104 | 105 | 106 | ### .unlock(file, [options]) 107 | 108 | Releases a previously acquired lock on `file` or rejects the promise on error. 109 | 110 | Whenever possible you should use the `release` function instead (as exemplified above). Still there are cases in which it's hard to keep a reference to it around code. In those cases `unlock()` might be handy. 111 | 112 | Available options: 113 | 114 | - `realpath`: Resolve symlinks using realpath, defaults to `true` (note that if `true`, the `file` must exist previously) 115 | - `fs`: A custom fs to use, defaults to `graceful-fs` 116 | - `lockfilePath`: Custom lockfile path. e.g.: If you want to lock a directory and create the lock file inside it, you can pass `file` as `` and `options.lockfilePath` as `/dir.lock` 117 | 118 | 119 | ```js 120 | const lockfile = require('proper-lockfile'); 121 | 122 | lockfile.lock('some/file') 123 | .then(() => { 124 | // Do something while the file is locked 125 | 126 | // Later.. 127 | return lockfile.unlock('some/file'); 128 | }); 129 | ``` 130 | 131 | ### .check(file, [options]) 132 | 133 | Check if the file is locked and its lockfile is not stale, rejects the promise on error. 134 | 135 | Available options: 136 | 137 | - `stale`: Duration in milliseconds in which the lock is considered stale, defaults to `10000` (minimum value is `5000`) 138 | - `realpath`: Resolve symlinks using realpath, defaults to `true` (note that if `true`, the `file` must exist previously) 139 | - `fs`: A custom fs to use, defaults to `graceful-fs` 140 | - `lockfilePath`: Custom lockfile path. e.g.: If you want to lock a directory and create the lock file inside it, you can pass `file` as `` and `options.lockfilePath` as `/dir.lock` 141 | 142 | 143 | ```js 144 | const lockfile = require('proper-lockfile'); 145 | 146 | lockfile.check('some/file') 147 | .then((isLocked) => { 148 | // isLocked will be true if 'some/file' is locked, false otherwise 149 | }); 150 | ``` 151 | 152 | ### .lockSync(file, [options]) 153 | 154 | Sync version of `.lock()`. 155 | Returns the `release` function or throws on error. 156 | 157 | ### .unlockSync(file, [options]) 158 | 159 | Sync version of `.unlock()`. 160 | Throws on error. 161 | 162 | ### .checkSync(file, [options]) 163 | 164 | Sync version of `.check()`. 165 | Returns a boolean or throws on error. 166 | 167 | 168 | ## Graceful exit 169 | 170 | `proper-lockfile` automatically removes locks if the process exits, except if the process is killed with SIGKILL or it crashes due to a VM fatal error (e.g.: out of memory). 171 | 172 | 173 | ## Tests 174 | 175 | `$ npm test` 176 | `$ npm test -- --watch` during development 177 | 178 | The test suite is very extensive. There's even a stress test to guarantee exclusiveness of locks. 179 | 180 | 181 | ## License 182 | 183 | Released under the [MIT License](https://www.opensource.org/licenses/mit-license.php). 184 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const lockfile = require('./lib/lockfile'); 4 | const { toPromise, toSync, toSyncOptions } = require('./lib/adapter'); 5 | 6 | async function lock(file, options) { 7 | const release = await toPromise(lockfile.lock)(file, options); 8 | 9 | return toPromise(release); 10 | } 11 | 12 | function lockSync(file, options) { 13 | const release = toSync(lockfile.lock)(file, toSyncOptions(options)); 14 | 15 | return toSync(release); 16 | } 17 | 18 | function unlock(file, options) { 19 | return toPromise(lockfile.unlock)(file, options); 20 | } 21 | 22 | function unlockSync(file, options) { 23 | return toSync(lockfile.unlock)(file, toSyncOptions(options)); 24 | } 25 | 26 | function check(file, options) { 27 | return toPromise(lockfile.check)(file, options); 28 | } 29 | 30 | function checkSync(file, options) { 31 | return toSync(lockfile.check)(file, toSyncOptions(options)); 32 | } 33 | 34 | module.exports = lock; 35 | module.exports.lock = lock; 36 | module.exports.unlock = unlock; 37 | module.exports.lockSync = lockSync; 38 | module.exports.unlockSync = unlockSync; 39 | module.exports.check = check; 40 | module.exports.checkSync = checkSync; 41 | -------------------------------------------------------------------------------- /lib/adapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | 5 | function createSyncFs(fs) { 6 | const methods = ['mkdir', 'realpath', 'stat', 'rmdir', 'utimes']; 7 | const newFs = { ...fs }; 8 | 9 | methods.forEach((method) => { 10 | newFs[method] = (...args) => { 11 | const callback = args.pop(); 12 | let ret; 13 | 14 | try { 15 | ret = fs[`${method}Sync`](...args); 16 | } catch (err) { 17 | return callback(err); 18 | } 19 | 20 | callback(null, ret); 21 | }; 22 | }); 23 | 24 | return newFs; 25 | } 26 | 27 | // ---------------------------------------------------------- 28 | 29 | function toPromise(method) { 30 | return (...args) => new Promise((resolve, reject) => { 31 | args.push((err, result) => { 32 | if (err) { 33 | reject(err); 34 | } else { 35 | resolve(result); 36 | } 37 | }); 38 | 39 | method(...args); 40 | }); 41 | } 42 | 43 | function toSync(method) { 44 | return (...args) => { 45 | let err; 46 | let result; 47 | 48 | args.push((_err, _result) => { 49 | err = _err; 50 | result = _result; 51 | }); 52 | 53 | method(...args); 54 | 55 | if (err) { 56 | throw err; 57 | } 58 | 59 | return result; 60 | }; 61 | } 62 | 63 | function toSyncOptions(options) { 64 | // Shallow clone options because we are oging to mutate them 65 | options = { ...options }; 66 | 67 | // Transform fs to use the sync methods instead 68 | options.fs = createSyncFs(options.fs || fs); 69 | 70 | // Retries are not allowed because it requires the flow to be sync 71 | if ( 72 | (typeof options.retries === 'number' && options.retries > 0) || 73 | (options.retries && typeof options.retries.retries === 'number' && options.retries.retries > 0) 74 | ) { 75 | throw Object.assign(new Error('Cannot use retries with the sync api'), { code: 'ESYNC' }); 76 | } 77 | 78 | return options; 79 | } 80 | 81 | module.exports = { 82 | toPromise, 83 | toSync, 84 | toSyncOptions, 85 | }; 86 | -------------------------------------------------------------------------------- /lib/lockfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('graceful-fs'); 5 | const retry = require('retry'); 6 | const onExit = require('signal-exit'); 7 | const mtimePrecision = require('./mtime-precision'); 8 | 9 | const locks = {}; 10 | 11 | function getLockFile(file, options) { 12 | return options.lockfilePath || `${file}.lock`; 13 | } 14 | 15 | function resolveCanonicalPath(file, options, callback) { 16 | if (!options.realpath) { 17 | return callback(null, path.resolve(file)); 18 | } 19 | 20 | // Use realpath to resolve symlinks 21 | // It also resolves relative paths 22 | options.fs.realpath(file, callback); 23 | } 24 | 25 | function acquireLock(file, options, callback) { 26 | const lockfilePath = getLockFile(file, options); 27 | 28 | // Use mkdir to create the lockfile (atomic operation) 29 | options.fs.mkdir(lockfilePath, (err) => { 30 | if (!err) { 31 | // At this point, we acquired the lock! 32 | // Probe the mtime precision 33 | return mtimePrecision.probe(lockfilePath, options.fs, (err, mtime, mtimePrecision) => { 34 | // If it failed, try to remove the lock.. 35 | /* istanbul ignore if */ 36 | if (err) { 37 | options.fs.rmdir(lockfilePath, () => {}); 38 | 39 | return callback(err); 40 | } 41 | 42 | callback(null, mtime, mtimePrecision); 43 | }); 44 | } 45 | 46 | // If error is not EEXIST then some other error occurred while locking 47 | if (err.code !== 'EEXIST') { 48 | return callback(err); 49 | } 50 | 51 | // Otherwise, check if lock is stale by analyzing the file mtime 52 | if (options.stale <= 0) { 53 | return callback(Object.assign(new Error('Lock file is already being held'), { code: 'ELOCKED', file })); 54 | } 55 | 56 | options.fs.stat(lockfilePath, (err, stat) => { 57 | if (err) { 58 | // Retry if the lockfile has been removed (meanwhile) 59 | // Skip stale check to avoid recursiveness 60 | if (err.code === 'ENOENT') { 61 | return acquireLock(file, { ...options, stale: 0 }, callback); 62 | } 63 | 64 | return callback(err); 65 | } 66 | 67 | if (!isLockStale(stat, options)) { 68 | return callback(Object.assign(new Error('Lock file is already being held'), { code: 'ELOCKED', file })); 69 | } 70 | 71 | // If it's stale, remove it and try again! 72 | // Skip stale check to avoid recursiveness 73 | removeLock(file, options, (err) => { 74 | if (err) { 75 | return callback(err); 76 | } 77 | 78 | acquireLock(file, { ...options, stale: 0 }, callback); 79 | }); 80 | }); 81 | }); 82 | } 83 | 84 | function isLockStale(stat, options) { 85 | return stat.mtime.getTime() < Date.now() - options.stale; 86 | } 87 | 88 | function removeLock(file, options, callback) { 89 | // Remove lockfile, ignoring ENOENT errors 90 | options.fs.rmdir(getLockFile(file, options), (err) => { 91 | if (err && err.code !== 'ENOENT') { 92 | return callback(err); 93 | } 94 | 95 | callback(); 96 | }); 97 | } 98 | 99 | function updateLock(file, options) { 100 | const lock = locks[file]; 101 | 102 | // Just for safety, should never happen 103 | /* istanbul ignore if */ 104 | if (lock.updateTimeout) { 105 | return; 106 | } 107 | 108 | lock.updateDelay = lock.updateDelay || options.update; 109 | lock.updateTimeout = setTimeout(() => { 110 | lock.updateTimeout = null; 111 | 112 | // Stat the file to check if mtime is still ours 113 | // If it is, we can still recover from a system sleep or a busy event loop 114 | options.fs.stat(lock.lockfilePath, (err, stat) => { 115 | const isOverThreshold = lock.lastUpdate + options.stale < Date.now(); 116 | 117 | // If it failed to update the lockfile, keep trying unless 118 | // the lockfile was deleted or we are over the threshold 119 | if (err) { 120 | if (err.code === 'ENOENT' || isOverThreshold) { 121 | return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' })); 122 | } 123 | 124 | lock.updateDelay = 1000; 125 | 126 | return updateLock(file, options); 127 | } 128 | 129 | const isMtimeOurs = lock.mtime.getTime() === stat.mtime.getTime(); 130 | 131 | if (!isMtimeOurs) { 132 | return setLockAsCompromised( 133 | file, 134 | lock, 135 | Object.assign( 136 | new Error('Unable to update lock within the stale threshold'), 137 | { code: 'ECOMPROMISED' } 138 | )); 139 | } 140 | 141 | const mtime = mtimePrecision.getMtime(lock.mtimePrecision); 142 | 143 | options.fs.utimes(lock.lockfilePath, mtime, mtime, (err) => { 144 | const isOverThreshold = lock.lastUpdate + options.stale < Date.now(); 145 | 146 | // Ignore if the lock was released 147 | if (lock.released) { 148 | return; 149 | } 150 | 151 | // If it failed to update the lockfile, keep trying unless 152 | // the lockfile was deleted or we are over the threshold 153 | if (err) { 154 | if (err.code === 'ENOENT' || isOverThreshold) { 155 | return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' })); 156 | } 157 | 158 | lock.updateDelay = 1000; 159 | 160 | return updateLock(file, options); 161 | } 162 | 163 | // All ok, keep updating.. 164 | lock.mtime = mtime; 165 | lock.lastUpdate = Date.now(); 166 | lock.updateDelay = null; 167 | updateLock(file, options); 168 | }); 169 | }); 170 | }, lock.updateDelay); 171 | 172 | // Unref the timer so that the nodejs process can exit freely 173 | // This is safe because all acquired locks will be automatically released 174 | // on process exit 175 | 176 | // We first check that `lock.updateTimeout.unref` exists because some users 177 | // may be using this module outside of NodeJS (e.g., in an electron app), 178 | // and in those cases `setTimeout` return an integer. 179 | /* istanbul ignore else */ 180 | if (lock.updateTimeout.unref) { 181 | lock.updateTimeout.unref(); 182 | } 183 | } 184 | 185 | function setLockAsCompromised(file, lock, err) { 186 | // Signal the lock has been released 187 | lock.released = true; 188 | 189 | // Cancel lock mtime update 190 | // Just for safety, at this point updateTimeout should be null 191 | /* istanbul ignore if */ 192 | if (lock.updateTimeout) { 193 | clearTimeout(lock.updateTimeout); 194 | } 195 | 196 | if (locks[file] === lock) { 197 | delete locks[file]; 198 | } 199 | 200 | lock.options.onCompromised(err); 201 | } 202 | 203 | // ---------------------------------------------------------- 204 | 205 | function lock(file, options, callback) { 206 | /* istanbul ignore next */ 207 | options = { 208 | stale: 10000, 209 | update: null, 210 | realpath: true, 211 | retries: 0, 212 | fs, 213 | onCompromised: (err) => { throw err; }, 214 | ...options, 215 | }; 216 | 217 | options.retries = options.retries || 0; 218 | options.retries = typeof options.retries === 'number' ? { retries: options.retries } : options.retries; 219 | options.stale = Math.max(options.stale || 0, 2000); 220 | options.update = options.update == null ? options.stale / 2 : options.update || 0; 221 | options.update = Math.max(Math.min(options.update, options.stale / 2), 1000); 222 | 223 | // Resolve to a canonical file path 224 | resolveCanonicalPath(file, options, (err, file) => { 225 | if (err) { 226 | return callback(err); 227 | } 228 | 229 | // Attempt to acquire the lock 230 | const operation = retry.operation(options.retries); 231 | 232 | operation.attempt(() => { 233 | acquireLock(file, options, (err, mtime, mtimePrecision) => { 234 | if (operation.retry(err)) { 235 | return; 236 | } 237 | 238 | if (err) { 239 | return callback(operation.mainError()); 240 | } 241 | 242 | // We now own the lock 243 | const lock = locks[file] = { 244 | lockfilePath: getLockFile(file, options), 245 | mtime, 246 | mtimePrecision, 247 | options, 248 | lastUpdate: Date.now(), 249 | }; 250 | 251 | // We must keep the lock fresh to avoid staleness 252 | updateLock(file, options); 253 | 254 | callback(null, (releasedCallback) => { 255 | if (lock.released) { 256 | return releasedCallback && 257 | releasedCallback(Object.assign(new Error('Lock is already released'), { code: 'ERELEASED' })); 258 | } 259 | 260 | // Not necessary to use realpath twice when unlocking 261 | unlock(file, { ...options, realpath: false }, releasedCallback); 262 | }); 263 | }); 264 | }); 265 | }); 266 | } 267 | 268 | function unlock(file, options, callback) { 269 | options = { 270 | fs, 271 | realpath: true, 272 | ...options, 273 | }; 274 | 275 | // Resolve to a canonical file path 276 | resolveCanonicalPath(file, options, (err, file) => { 277 | if (err) { 278 | return callback(err); 279 | } 280 | 281 | // Skip if the lock is not acquired 282 | const lock = locks[file]; 283 | 284 | if (!lock) { 285 | return callback(Object.assign(new Error('Lock is not acquired/owned by you'), { code: 'ENOTACQUIRED' })); 286 | } 287 | 288 | lock.updateTimeout && clearTimeout(lock.updateTimeout); // Cancel lock mtime update 289 | lock.released = true; // Signal the lock has been released 290 | delete locks[file]; // Delete from locks 291 | 292 | removeLock(file, options, callback); 293 | }); 294 | } 295 | 296 | function check(file, options, callback) { 297 | options = { 298 | stale: 10000, 299 | realpath: true, 300 | fs, 301 | ...options, 302 | }; 303 | 304 | options.stale = Math.max(options.stale || 0, 2000); 305 | 306 | // Resolve to a canonical file path 307 | resolveCanonicalPath(file, options, (err, file) => { 308 | if (err) { 309 | return callback(err); 310 | } 311 | 312 | // Check if lockfile exists 313 | options.fs.stat(getLockFile(file, options), (err, stat) => { 314 | if (err) { 315 | // If does not exist, file is not locked. Otherwise, callback with error 316 | return err.code === 'ENOENT' ? callback(null, false) : callback(err); 317 | } 318 | 319 | // Otherwise, check if lock is stale by analyzing the file mtime 320 | return callback(null, !isLockStale(stat, options)); 321 | }); 322 | }); 323 | } 324 | 325 | function getLocks() { 326 | return locks; 327 | } 328 | 329 | // Remove acquired locks on exit 330 | /* istanbul ignore next */ 331 | onExit(() => { 332 | for (const file in locks) { 333 | const options = locks[file].options; 334 | 335 | try { options.fs.rmdirSync(getLockFile(file, options)); } catch (e) { /* Empty */ } 336 | } 337 | }); 338 | 339 | module.exports.lock = lock; 340 | module.exports.unlock = unlock; 341 | module.exports.check = check; 342 | module.exports.getLocks = getLocks; 343 | -------------------------------------------------------------------------------- /lib/mtime-precision.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cacheSymbol = Symbol(); 4 | 5 | function probe(file, fs, callback) { 6 | const cachedPrecision = fs[cacheSymbol]; 7 | 8 | if (cachedPrecision) { 9 | return fs.stat(file, (err, stat) => { 10 | /* istanbul ignore if */ 11 | if (err) { 12 | return callback(err); 13 | } 14 | 15 | callback(null, stat.mtime, cachedPrecision); 16 | }); 17 | } 18 | 19 | // Set mtime by ceiling Date.now() to seconds + 5ms so that it's "not on the second" 20 | const mtime = new Date((Math.ceil(Date.now() / 1000) * 1000) + 5); 21 | 22 | fs.utimes(file, mtime, mtime, (err) => { 23 | /* istanbul ignore if */ 24 | if (err) { 25 | return callback(err); 26 | } 27 | 28 | fs.stat(file, (err, stat) => { 29 | /* istanbul ignore if */ 30 | if (err) { 31 | return callback(err); 32 | } 33 | 34 | const precision = stat.mtime.getTime() % 1000 === 0 ? 's' : 'ms'; 35 | 36 | // Cache the precision in a non-enumerable way 37 | Object.defineProperty(fs, cacheSymbol, { value: precision }); 38 | 39 | callback(null, stat.mtime, precision); 40 | }); 41 | }); 42 | } 43 | 44 | function getMtime(precision) { 45 | let now = Date.now(); 46 | 47 | if (precision === 's') { 48 | now = Math.ceil(now / 1000) * 1000; 49 | } 50 | 51 | return new Date(now); 52 | } 53 | 54 | module.exports.probe = probe; 55 | module.exports.getMtime = getMtime; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proper-lockfile", 3 | "version": "4.1.2", 4 | "description": "A inter-process and inter-machine lockfile utility that works on a local or network file system", 5 | "keywords": [ 6 | "lock", 7 | "locking", 8 | "file", 9 | "lockfile", 10 | "fs", 11 | "cross-process" 12 | ], 13 | "author": "André Cruz ", 14 | "homepage": "https://github.com/moxystudio/node-proper-lockfile", 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com:moxystudio/node-proper-lockfile.git" 18 | }, 19 | "license": "MIT", 20 | "main": "index.js", 21 | "files": [ 22 | "lib" 23 | ], 24 | "scripts": { 25 | "lint": "eslint .", 26 | "test": "jest --env node --coverage --runInBand", 27 | "prerelease": "npm t && npm run lint", 28 | "release": "standard-version", 29 | "postrelease": "git push --follow-tags origin HEAD && npm publish" 30 | }, 31 | "husky": { 32 | "hooks": { 33 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 34 | "pre-commit": "lint-staged" 35 | } 36 | }, 37 | "lint-staged": { 38 | "*.js": [ 39 | "eslint --fix", 40 | "git add" 41 | ] 42 | }, 43 | "commitlint": { 44 | "extends": [ 45 | "@commitlint/config-conventional" 46 | ] 47 | }, 48 | "dependencies": { 49 | "graceful-fs": "^4.2.4", 50 | "retry": "^0.12.0", 51 | "signal-exit": "^3.0.2" 52 | }, 53 | "devDependencies": { 54 | "@commitlint/cli": "^7.0.0", 55 | "@commitlint/config-conventional": "^7.0.1", 56 | "@segment/clear-timeouts": "^2.0.0", 57 | "delay": "^4.1.0", 58 | "eslint": "^5.3.0", 59 | "eslint-config-moxy": "^7.1.0", 60 | "execa": "^1.0.0", 61 | "husky": "^1.1.4", 62 | "jest": "^24.5.0", 63 | "lint-staged": "^8.0.4", 64 | "mkdirp": "^0.5.1", 65 | "p-defer": "^2.1.0", 66 | "rimraf": "^2.6.2", 67 | "stable": "^0.1.8", 68 | "standard-version": "^5.0.0", 69 | "thread-sleep": "^2.1.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/check.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | const rimraf = require('rimraf'); 5 | const pDelay = require('delay'); 6 | const mkdirp = require('mkdirp'); 7 | const lockfile = require('../'); 8 | const unlockAll = require('./util/unlockAll'); 9 | 10 | const tmpDir = `${__dirname}/tmp`; 11 | 12 | beforeAll(() => mkdirp.sync(tmpDir)); 13 | 14 | afterAll(() => rimraf.sync(tmpDir)); 15 | 16 | afterEach(async () => { 17 | await unlockAll(); 18 | rimraf.sync(`${tmpDir}/*`); 19 | }); 20 | 21 | it('should fail if the file does not exist by default', async () => { 22 | expect.assertions(1); 23 | 24 | try { 25 | await lockfile.check(`${tmpDir}/some-file-that-will-never-exist`); 26 | } catch (err) { 27 | expect(err.code).toBe('ENOENT'); 28 | } 29 | }); 30 | 31 | it('should not fail if the file does not exist and realpath is false', async () => { 32 | await lockfile.check(`${tmpDir}/some-file-that-will-never-exist`, { realpath: false }); 33 | }); 34 | 35 | it('should return a promise', async () => { 36 | fs.writeFileSync(`${tmpDir}/foo`, ''); 37 | 38 | const promise = lockfile.check(`${tmpDir}/foo`); 39 | 40 | expect(typeof promise.then).toBe('function'); 41 | 42 | await promise; 43 | }); 44 | 45 | it('should resolve with true if file is locked', async () => { 46 | fs.writeFileSync(`${tmpDir}/foo`, ''); 47 | 48 | await lockfile.lock(`${tmpDir}/foo`); 49 | 50 | const isLocked = await lockfile.check(`${tmpDir}/foo`); 51 | 52 | expect(isLocked).toBe(true); 53 | }); 54 | 55 | it('should resolve with false if file is not locked', async () => { 56 | fs.writeFileSync(`${tmpDir}/foo`, ''); 57 | 58 | const isLocked = await lockfile.check(`${tmpDir}/foo`); 59 | 60 | expect(isLocked).toBe(false); 61 | }); 62 | 63 | it('should use the custom fs', async () => { 64 | const customFs = { 65 | ...fs, 66 | realpath: (path, callback) => callback(new Error('foo')), 67 | }; 68 | 69 | expect.assertions(1); 70 | 71 | try { 72 | await lockfile.check(`${tmpDir}/foo`, { fs: customFs }); 73 | } catch (err) { 74 | expect(err.message).toBe('foo'); 75 | } 76 | }); 77 | 78 | it('should resolve symlinks by default', async () => { 79 | fs.writeFileSync(`${tmpDir}/foo`, ''); 80 | fs.symlinkSync(`${tmpDir}/foo`, `${tmpDir}/bar`); 81 | 82 | await lockfile.lock(`${tmpDir}/bar`); 83 | 84 | let isLocked = await lockfile.check(`${tmpDir}/bar`); 85 | 86 | expect(isLocked).toBe(true); 87 | 88 | isLocked = await lockfile.check(`${tmpDir}/foo`); 89 | 90 | expect(isLocked).toBe(true); 91 | }); 92 | 93 | it('should not resolve symlinks if realpath is false', async () => { 94 | fs.writeFileSync(`${tmpDir}/foo`, ''); 95 | fs.symlinkSync(`${tmpDir}/foo`, `${tmpDir}/bar`); 96 | 97 | await lockfile.lock(`${tmpDir}/bar`, { realpath: false }); 98 | 99 | let isLocked = await lockfile.check(`${tmpDir}/bar`, { realpath: false }); 100 | 101 | expect(isLocked).toBe(true); 102 | 103 | isLocked = await lockfile.check(`${tmpDir}/foo`, { realpath: false }); 104 | 105 | expect(isLocked).toBe(false); 106 | }); 107 | 108 | it('should fail if stating the lockfile errors out when verifying staleness', async () => { 109 | fs.writeFileSync(`${tmpDir}/foo`, ''); 110 | 111 | const mtime = new Date(Date.now() - 60000); 112 | const customFs = { 113 | ...fs, 114 | stat: (path, callback) => callback(new Error('foo')), 115 | }; 116 | 117 | fs.mkdirSync(`${tmpDir}/foo.lock`); 118 | fs.utimesSync(`${tmpDir}/foo.lock`, mtime, mtime); 119 | 120 | expect.assertions(1); 121 | 122 | try { 123 | await lockfile.check(`${tmpDir}/foo`, { fs: customFs }); 124 | } catch (err) { 125 | expect(err.message).toBe('foo'); 126 | } 127 | }); 128 | 129 | it('should set stale to a minimum of 2000', async () => { 130 | fs.writeFileSync(`${tmpDir}/foo`, ''); 131 | fs.mkdirSync(`${tmpDir}/foo.lock`); 132 | 133 | expect.assertions(2); 134 | 135 | await pDelay(200); 136 | 137 | let isLocked = await lockfile.check(`${tmpDir}/foo`, { stale: 100 }); 138 | 139 | expect(isLocked).toBe(true); 140 | 141 | await pDelay(2000); 142 | 143 | isLocked = await lockfile.check(`${tmpDir}/foo`, { stale: 100 }); 144 | 145 | expect(isLocked).toBe(false); 146 | }); 147 | 148 | it('should set stale to a minimum of 2000 (falsy)', async () => { 149 | fs.writeFileSync(`${tmpDir}/foo`, ''); 150 | fs.mkdirSync(`${tmpDir}/foo.lock`); 151 | 152 | expect.assertions(2); 153 | 154 | await pDelay(200); 155 | 156 | let isLocked = await lockfile.check(`${tmpDir}/foo`, { stale: false }); 157 | 158 | expect(isLocked).toBe(true); 159 | 160 | await pDelay(2000); 161 | 162 | isLocked = await lockfile.check(`${tmpDir}/foo`, { stale: false }); 163 | 164 | expect(isLocked).toBe(false); 165 | }); 166 | -------------------------------------------------------------------------------- /test/fixtures/compromised.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const lockfile = require('../..'); 5 | 6 | const tmpDir = `${__dirname}/../tmp`; 7 | 8 | fs.writeFileSync(`${tmpDir}/foo`, ''); 9 | 10 | lockfile.lockSync(`${tmpDir}/foo`, { update: 1000 }); 11 | 12 | fs.rmdirSync(`${tmpDir}/foo.lock`); 13 | 14 | // Do not let the process exit 15 | setInterval(() => {}, 1000); 16 | 17 | process.on('uncaughtException', (err) => { 18 | err.code && process.stderr.write(`${err.code}\n\n`); 19 | throw err; 20 | }); 21 | -------------------------------------------------------------------------------- /test/fixtures/crash.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const lockfile = require('../..'); 5 | 6 | const tmpDir = `${__dirname}/../tmp`; 7 | 8 | fs.writeFileSync(`${tmpDir}/foo`, ''); 9 | 10 | lockfile.lockSync(`${tmpDir}/foo`); 11 | 12 | throw new Error('intencional crash'); 13 | -------------------------------------------------------------------------------- /test/fixtures/stress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cluster = require('cluster'); 4 | const fs = require('fs'); 5 | const os = require('os'); 6 | const pDelay = require('delay'); 7 | const sort = require('stable'); 8 | const lockfile = require('../../'); 9 | 10 | const tmpDir = `${__dirname}/../tmp`; 11 | 12 | const maxTryDelay = 50; 13 | const maxLockTime = 200; 14 | const totalTestTime = 60000; 15 | 16 | function printExcerpt(logs, index) { 17 | const startIndex = Math.max(0, index - 50); 18 | const endIndex = index + 50; 19 | 20 | logs 21 | .slice(startIndex, endIndex) 22 | .forEach((log, index) => process.stdout.write(`${startIndex + index + 1} ${log.timestamp} ${log.message}\n`)); 23 | } 24 | 25 | async function master() { 26 | const numCPUs = os.cpus().length; 27 | let logs = []; 28 | 29 | fs.writeFileSync(`${tmpDir}/foo`, ''); 30 | 31 | for (let i = 0; i < numCPUs; i += 1) { 32 | cluster.fork(); 33 | } 34 | 35 | cluster.on('online', (worker) => 36 | worker.on('message', (data) => 37 | logs.push(data.toString().trim()))); 38 | 39 | cluster.on('exit', () => { 40 | throw new Error('Child died prematurely'); 41 | }); 42 | 43 | await pDelay(totalTestTime); 44 | 45 | cluster.removeAllListeners('exit'); 46 | 47 | cluster.disconnect(() => { 48 | let acquired; 49 | 50 | // Parse & sort logs 51 | logs = logs.map((log) => { 52 | const split = log.split(' '); 53 | 54 | return { timestamp: Number(split[0]), message: split[1] }; 55 | }); 56 | 57 | logs = sort(logs, (log1, log2) => { 58 | if (log1.timestamp > log2.timestamp) { 59 | return 1; 60 | } 61 | if (log1.timestamp < log2.timestamp) { 62 | return -1; 63 | } 64 | if (log1.message === 'LOCK_RELEASE_CALLED') { 65 | return -1; 66 | } 67 | if (log2.message === 'LOCK_RELEASE_CALLED') { 68 | return 1; 69 | } 70 | 71 | return 0; 72 | }); 73 | 74 | // Validate logs 75 | logs.forEach((log, index) => { 76 | switch (log.message) { 77 | case 'LOCK_ACQUIRED': 78 | if (acquired) { 79 | process.stdout.write(`\nInconsistent at line ${index + 1}\n`); 80 | printExcerpt(logs, index); 81 | process.exit(1); 82 | } 83 | 84 | acquired = true; 85 | break; 86 | case 'LOCK_RELEASE_CALLED': 87 | if (!acquired) { 88 | process.stdout.write(`\nInconsistent at line ${index + 1}\n`); 89 | printExcerpt(logs, index); 90 | process.exit(1); 91 | } 92 | 93 | acquired = false; 94 | break; 95 | default: 96 | // Do nothing 97 | } 98 | }); 99 | 100 | process.exit(0); 101 | }); 102 | } 103 | 104 | function worker() { 105 | process.on('disconnect', () => process.exit(0)); 106 | 107 | const tryLock = async () => { 108 | await pDelay(Math.max(Math.random(), 10) * maxTryDelay); 109 | 110 | process.send(`${Date.now()} LOCK_TRY\n`); 111 | 112 | let release; 113 | 114 | try { 115 | release = await lockfile.lock(`${tmpDir}/foo`); 116 | } catch (err) { 117 | process.send(`${Date.now()} LOCK_BUSY\n`); 118 | tryLock(); 119 | 120 | return; 121 | } 122 | 123 | process.send(`${Date.now()} LOCK_ACQUIRED\n`); 124 | 125 | await pDelay(Math.max(Math.random(), 10) * maxLockTime); 126 | 127 | process.send(`${Date.now()} LOCK_RELEASE_CALLED\n`); 128 | 129 | await release(); 130 | 131 | tryLock(); 132 | }; 133 | 134 | tryLock(); 135 | } 136 | 137 | // Any unhandled promise should cause the process to exit 138 | process.on('unhandledRejection', (err) => { 139 | console.error(err.stack); 140 | process.exit(1); 141 | }); 142 | 143 | if (cluster.isMaster) { 144 | master(); 145 | } else { 146 | worker(); 147 | } 148 | -------------------------------------------------------------------------------- /test/fixtures/unref.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const lockfile = require('../../'); 5 | 6 | const tmpDir = `${__dirname}/../tmp`; 7 | 8 | fs.writeFileSync(`${tmpDir}/foo`, ''); 9 | 10 | lockfile.lockSync(`${tmpDir}/foo`); 11 | -------------------------------------------------------------------------------- /test/lock.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | const mkdirp = require('mkdirp'); 5 | const sleep = require('thread-sleep'); 6 | const rimraf = require('rimraf'); 7 | const execa = require('execa'); 8 | const pDefer = require('p-defer'); 9 | const pDelay = require('delay'); 10 | const clearTimeouts = require('@segment/clear-timeouts'); 11 | const lockfile = require('../'); 12 | const unlockAll = require('./util/unlockAll'); 13 | 14 | const tmpDir = `${__dirname}/tmp`; 15 | 16 | clearTimeouts.install(); 17 | 18 | beforeAll(() => mkdirp.sync(tmpDir)); 19 | 20 | afterAll(() => rimraf.sync(tmpDir)); 21 | 22 | afterEach(async () => { 23 | jest.restoreAllMocks(); 24 | clearTimeouts(); 25 | 26 | await unlockAll(); 27 | rimraf.sync(`${tmpDir}/*`); 28 | }); 29 | 30 | it('should be the default export', () => { 31 | expect(lockfile).toBe(lockfile.lock); 32 | }); 33 | 34 | it('should fail if the file does not exist by default', async () => { 35 | expect.assertions(1); 36 | 37 | try { 38 | await lockfile.lock(`${tmpDir}/some-file-that-will-never-exist`); 39 | } catch (err) { 40 | expect(err.code).toBe('ENOENT'); 41 | } 42 | }); 43 | 44 | it('should not fail if the file does not exist and realpath is false', async () => { 45 | await lockfile.lock(`${tmpDir}/some-file-that-will-never-exist`, { realpath: false }); 46 | }); 47 | 48 | it('should fail if impossible to create the lockfile because directory does not exist', async () => { 49 | expect.assertions(1); 50 | 51 | try { 52 | await lockfile.lock(`${tmpDir}/some-dir-that-will-never-exist/foo`); 53 | } catch (err) { 54 | expect(err.code).toBe('ENOENT'); 55 | } 56 | }); 57 | 58 | it('should return a promise for a release function', async () => { 59 | fs.writeFileSync(`${tmpDir}/foo`, ''); 60 | 61 | const promise = lockfile.lock(`${tmpDir}/foo`); 62 | 63 | expect(typeof promise.then).toBe('function'); 64 | 65 | const release = await promise; 66 | 67 | expect(typeof release).toBe('function'); 68 | }); 69 | 70 | it('should create the lockfile', async () => { 71 | fs.writeFileSync(`${tmpDir}/foo`, ''); 72 | 73 | await lockfile.lock(`${tmpDir}/foo`); 74 | 75 | expect(fs.existsSync(`${tmpDir}/foo`)).toBe(true); 76 | }); 77 | 78 | it('should create the lockfile inside a folder', async () => { 79 | fs.mkdirSync(`${tmpDir}/foo-dir`); 80 | 81 | await lockfile.lock(`${tmpDir}/foo-dir`, { lockfilePath: `${tmpDir}/foo-dir/dir.lock` }); 82 | 83 | expect(fs.existsSync(`${tmpDir}/foo-dir/dir.lock`)).toBe(true); 84 | }); 85 | 86 | it('should fail if already locked', async () => { 87 | fs.writeFileSync(`${tmpDir}/foo`, ''); 88 | 89 | expect.assertions(1); 90 | 91 | await lockfile.lock(`${tmpDir}/foo`); 92 | 93 | try { 94 | await lockfile.lock(`${tmpDir}/foo`); 95 | } catch (err) { 96 | expect(err.code).toBe('ELOCKED'); 97 | } 98 | }); 99 | 100 | it('should fail if mkdir fails for an unknown reason', async () => { 101 | fs.writeFileSync(`${tmpDir}/foo`, ''); 102 | 103 | const customFs = { 104 | ...fs, 105 | mkdir: (path, callback) => callback(new Error('foo')), 106 | }; 107 | 108 | expect.assertions(1); 109 | 110 | try { 111 | await lockfile.lock(`${tmpDir}/foo`, { fs: customFs }); 112 | } catch (err) { 113 | expect(err.message).toBe('foo'); 114 | } 115 | }); 116 | 117 | it('should retry several times if retries were specified', async () => { 118 | fs.writeFileSync(`${tmpDir}/foo`, ''); 119 | 120 | const release = await lockfile.lock(`${tmpDir}/foo`); 121 | 122 | setTimeout(release, 4000); 123 | 124 | await lockfile.lock(`${tmpDir}/foo`, { retries: { retries: 5, maxTimeout: 1000 } }); 125 | }); 126 | 127 | it('should use a custom fs', async () => { 128 | const customFs = { 129 | ...fs, 130 | realpath: (path, callback) => callback(new Error('foo')), 131 | }; 132 | 133 | expect.assertions(1); 134 | 135 | try { 136 | await lockfile.lock(`${tmpDir}/foo`, { fs: customFs }); 137 | } catch (err) { 138 | expect(err.message).toBe('foo'); 139 | } 140 | }); 141 | 142 | it('should resolve symlinks by default', async () => { 143 | fs.writeFileSync(`${tmpDir}/foo`, ''); 144 | fs.symlinkSync(`${tmpDir}/foo`, `${tmpDir}/bar`); 145 | 146 | expect.assertions(2); 147 | 148 | await lockfile.lock(`${tmpDir}/bar`); 149 | 150 | try { 151 | await lockfile.lock(`${tmpDir}/bar`); 152 | } catch (err) { 153 | expect(err.code).toBe('ELOCKED'); 154 | } 155 | 156 | try { 157 | await lockfile.lock(`${tmpDir}/foo`); 158 | } catch (err) { 159 | expect(err.code).toBe('ELOCKED'); 160 | } 161 | }); 162 | 163 | it('should not resolve symlinks if realpath is false', async () => { 164 | fs.writeFileSync(`${tmpDir}/foo`, ''); 165 | fs.symlinkSync(`${tmpDir}/foo`, `${tmpDir}/bar`); 166 | 167 | await lockfile.lock(`${tmpDir}/bar`, { realpath: false }); 168 | await lockfile.lock(`${tmpDir}/foo`, { realpath: false }); 169 | 170 | expect(fs.existsSync(`${tmpDir}/bar.lock`)).toBe(true); 171 | expect(fs.existsSync(`${tmpDir}/foo.lock`)).toBe(true); 172 | }); 173 | 174 | it('should remove and acquire over stale locks', async () => { 175 | const mtime = new Date(Date.now() - 60000); 176 | 177 | fs.writeFileSync(`${tmpDir}/foo`, ''); 178 | fs.mkdirSync(`${tmpDir}/foo.lock`); 179 | fs.utimesSync(`${tmpDir}/foo.lock`, mtime, mtime); 180 | 181 | await lockfile.lock(`${tmpDir}/foo`); 182 | 183 | expect(fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime()).toBeGreaterThan(Date.now() - 3000); 184 | }); 185 | 186 | it('should retry if the lockfile was removed when verifying staleness', async () => { 187 | const mtime = new Date(Date.now() - 60000); 188 | let count = 0; 189 | const customFs = { 190 | ...fs, 191 | mkdir: jest.fn((...args) => fs.mkdir(...args)), 192 | stat: jest.fn((...args) => { 193 | if (count % 2 === 0) { 194 | rimraf.sync(`${tmpDir}/foo.lock`); 195 | } 196 | fs.stat(...args); 197 | count += 1; 198 | }), 199 | }; 200 | 201 | fs.writeFileSync(`${tmpDir}/foo`, ''); 202 | fs.mkdirSync(`${tmpDir}/foo.lock`); 203 | fs.utimesSync(`${tmpDir}/foo.lock`, mtime, mtime); 204 | 205 | await lockfile.lock(`${tmpDir}/foo`, { fs: customFs }); 206 | 207 | expect(customFs.mkdir).toHaveBeenCalledTimes(2); 208 | expect(customFs.stat).toHaveBeenCalledTimes(2); 209 | expect(fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime()).toBeGreaterThan(Date.now() - 3000); 210 | }); 211 | 212 | it('should retry if the lockfile was removed when verifying staleness (not recursively)', async () => { 213 | const mtime = new Date(Date.now() - 60000); 214 | const customFs = { 215 | ...fs, 216 | mkdir: jest.fn((...args) => fs.mkdir(...args)), 217 | stat: jest.fn((path, callback) => callback(Object.assign(new Error(), { code: 'ENOENT' }))), 218 | }; 219 | 220 | fs.writeFileSync(`${tmpDir}/foo`, ''); 221 | fs.mkdirSync(`${tmpDir}/foo.lock`); 222 | fs.utimesSync(`${tmpDir}/foo.lock`, mtime, mtime); 223 | 224 | expect.assertions(3); 225 | 226 | try { 227 | await lockfile.lock(`${tmpDir}/foo`, { fs: customFs }); 228 | } catch (err) { 229 | expect(err.code).toBe('ELOCKED'); 230 | expect(customFs.mkdir).toHaveBeenCalledTimes(2); 231 | expect(customFs.stat).toHaveBeenCalledTimes(1); 232 | } 233 | }); 234 | 235 | it('should fail if stating the lockfile errors out when verifying staleness', async () => { 236 | const mtime = new Date(Date.now() - 60000); 237 | const customFs = { 238 | ...fs, 239 | stat: (path, callback) => callback(new Error('foo')), 240 | }; 241 | 242 | fs.writeFileSync(`${tmpDir}/foo`, ''); 243 | fs.mkdirSync(`${tmpDir}/foo.lock`); 244 | fs.utimesSync(`${tmpDir}/foo.lock`, mtime, mtime); 245 | 246 | expect.assertions(1); 247 | 248 | try { 249 | await lockfile.lock(`${tmpDir}/foo`, { fs: customFs }); 250 | } catch (err) { 251 | expect(err.message).toBe('foo'); 252 | } 253 | }); 254 | 255 | it('should fail if removing a stale lockfile errors out', async () => { 256 | const mtime = new Date(Date.now() - 60000); 257 | const customFs = { 258 | ...fs, 259 | rmdir: (path, callback) => callback(new Error('foo')), 260 | }; 261 | 262 | customFs.rmdir = (path, callback) => { 263 | callback(new Error('foo')); 264 | }; 265 | 266 | fs.writeFileSync(`${tmpDir}/foo`, ''); 267 | fs.mkdirSync(`${tmpDir}/foo.lock`); 268 | fs.utimesSync(`${tmpDir}/foo.lock`, mtime, mtime); 269 | 270 | expect.assertions(1); 271 | 272 | try { 273 | await lockfile.lock(`${tmpDir}/foo`, { fs: customFs }); 274 | } catch (err) { 275 | expect(err.message).toBe('foo'); 276 | } 277 | }); 278 | 279 | it('should update the lockfile mtime automatically', async () => { 280 | fs.writeFileSync(`${tmpDir}/foo`, ''); 281 | 282 | await lockfile.lock(`${tmpDir}/foo`, { update: 1500 }); 283 | 284 | expect.assertions(2); 285 | 286 | let mtime = fs.statSync(`${tmpDir}/foo.lock`).mtime; 287 | 288 | // First update occurs at 1500ms 289 | await pDelay(2000); 290 | 291 | let stat = fs.statSync(`${tmpDir}/foo.lock`); 292 | 293 | expect(stat.mtime.getTime()).toBeGreaterThan(mtime.getTime()); 294 | mtime = stat.mtime; 295 | 296 | // Second update occurs at 3000ms 297 | await pDelay(2000); 298 | 299 | stat = fs.statSync(`${tmpDir}/foo.lock`); 300 | 301 | expect(stat.mtime.getTime()).toBeGreaterThan(mtime.getTime()); 302 | }); 303 | 304 | it('should set stale to a minimum of 2000', async () => { 305 | fs.writeFileSync(`${tmpDir}/foo`, ''); 306 | fs.mkdirSync(`${tmpDir}/foo.lock`); 307 | 308 | expect.assertions(1); 309 | 310 | await pDelay(200); 311 | 312 | try { 313 | await lockfile.lock(`${tmpDir}/foo`, { stale: 100 }); 314 | } catch (err) { 315 | expect(err.code).toBe('ELOCKED'); 316 | } 317 | 318 | await pDelay(2000); 319 | 320 | await lockfile.lock(`${tmpDir}/foo`, { stale: 100 }); 321 | }); 322 | 323 | it('should set stale to a minimum of 2000 (falsy)', async () => { 324 | fs.writeFileSync(`${tmpDir}/foo`, ''); 325 | fs.mkdirSync(`${tmpDir}/foo.lock`); 326 | 327 | expect.assertions(1); 328 | 329 | await pDelay(200); 330 | 331 | try { 332 | await lockfile.lock(`${tmpDir}/foo`, { stale: false }); 333 | } catch (err) { 334 | expect(err.code).toBe('ELOCKED'); 335 | } 336 | 337 | await pDelay(2000); 338 | 339 | await lockfile.lock(`${tmpDir}/foo`, { stale: false }); 340 | }); 341 | 342 | it('should call the compromised function if ENOENT was detected when updating the lockfile mtime', async () => { 343 | fs.writeFileSync(`${tmpDir}/foo`, ''); 344 | 345 | const deferred = pDefer(); 346 | 347 | const handleCompromised = async (err) => { 348 | expect(err.code).toBe('ECOMPROMISED'); 349 | expect(err.message).toMatch('ENOENT'); 350 | 351 | await lockfile.lock(`${tmpDir}/foo`); 352 | 353 | deferred.resolve(); 354 | }; 355 | 356 | await lockfile.lock(`${tmpDir}/foo`, { update: 1000, onCompromised: handleCompromised }); 357 | 358 | // Remove the file to trigger onCompromised 359 | rimraf.sync(`${tmpDir}/foo.lock`); 360 | 361 | await deferred.promise; 362 | }); 363 | 364 | it('should call the compromised function if failed to update the lockfile mtime too many times (stat)', async () => { 365 | fs.writeFileSync(`${tmpDir}/foo`, ''); 366 | 367 | const customFs = { ...fs }; 368 | 369 | const deferred = pDefer(); 370 | 371 | const handleCompromised = (err) => { 372 | expect(err.code).toBe('ECOMPROMISED'); 373 | expect(err.message).toMatch('foo'); 374 | 375 | deferred.resolve(); 376 | }; 377 | 378 | await lockfile.lock(`${tmpDir}/foo`, { 379 | fs: customFs, 380 | update: 1000, 381 | stale: 5000, 382 | onCompromised: handleCompromised, 383 | }); 384 | 385 | customFs.stat = (path, callback) => callback(new Error('foo')); 386 | 387 | await deferred.promise; 388 | }, 10000); 389 | 390 | it('should call the compromised function if failed to update the lockfile mtime too many times (utimes)', async () => { 391 | fs.writeFileSync(`${tmpDir}/foo`, ''); 392 | 393 | const customFs = { ...fs }; 394 | 395 | const deferred = pDefer(); 396 | 397 | const handleCompromised = (err) => { 398 | expect(err.code).toBe('ECOMPROMISED'); 399 | expect(err.message).toMatch('foo'); 400 | 401 | deferred.resolve(); 402 | }; 403 | 404 | await lockfile.lock(`${tmpDir}/foo`, { 405 | fs: customFs, 406 | update: 1000, 407 | stale: 5000, 408 | onCompromised: handleCompromised, 409 | }); 410 | 411 | customFs.utimes = (path, atime, mtime, callback) => callback(new Error('foo')); 412 | 413 | await deferred.promise; 414 | }, 10000); 415 | 416 | it('should call the compromised function if updating the lockfile took too much time', async () => { 417 | fs.writeFileSync(`${tmpDir}/foo`, ''); 418 | 419 | const customFs = { ...fs }; 420 | 421 | const deferred = pDefer(); 422 | 423 | const handleCompromised = (err) => { 424 | expect(err.code).toBe('ECOMPROMISED'); 425 | expect(err.message).toMatch('foo'); 426 | 427 | deferred.resolve(); 428 | }; 429 | 430 | await lockfile.lock(`${tmpDir}/foo`, { 431 | fs: customFs, 432 | update: 1000, 433 | stale: 5000, 434 | onCompromised: handleCompromised, 435 | }); 436 | 437 | customFs.utimes = (path, atime, mtime, callback) => setTimeout(() => callback(new Error('foo')), 6000); 438 | 439 | await deferred.promise; 440 | }, 10000); 441 | 442 | it('should call the compromised function if lock was acquired by someone else due to staleness', async () => { 443 | fs.writeFileSync(`${tmpDir}/foo`, ''); 444 | 445 | const customFs = { ...fs }; 446 | 447 | const deferred = pDefer(); 448 | 449 | const handleCompromised = (err) => { 450 | expect(err.code).toBe('ECOMPROMISED'); 451 | expect(fs.existsSync(`${tmpDir}/foo.lock`)).toBe(true); 452 | 453 | deferred.resolve(); 454 | }; 455 | 456 | await lockfile.lock(`${tmpDir}/foo`, { 457 | fs: customFs, 458 | update: 1000, 459 | stale: 3000, 460 | onCompromised: handleCompromised, 461 | }); 462 | 463 | customFs.utimes = (path, atime, mtime, callback) => setTimeout(() => callback(new Error('foo')), 6000); 464 | 465 | await pDelay(4500); 466 | 467 | await lockfile.lock(`${tmpDir}/foo`, { stale: 3000 }); 468 | 469 | await deferred.promise; 470 | }, 10000); 471 | 472 | it('should throw an error by default when the lock is compromised', async () => { 473 | const { stderr } = await execa('node', [`${__dirname}/fixtures/compromised.js`], { reject: false }); 474 | 475 | expect(stderr).toMatch('ECOMPROMISED'); 476 | }); 477 | 478 | it('should set update to a minimum of 1000', async () => { 479 | fs.writeFileSync(`${tmpDir}/foo`, ''); 480 | 481 | expect.assertions(2); 482 | 483 | await lockfile.lock(`${tmpDir}/foo`, { update: 100 }); 484 | 485 | const mtime = fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime(); 486 | 487 | await pDelay(200); 488 | 489 | expect(mtime).toBe(fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime()); 490 | 491 | await pDelay(1000); 492 | 493 | expect(fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime()).toBeGreaterThan(mtime); 494 | }, 10000); 495 | 496 | it('should set update to a minimum of 1000 (falsy)', async () => { 497 | fs.writeFileSync(`${tmpDir}/foo`, ''); 498 | 499 | expect.assertions(2); 500 | 501 | await lockfile.lock(`${tmpDir}/foo`, { update: false }); 502 | 503 | const mtime = fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime(); 504 | 505 | await pDelay(200); 506 | 507 | expect(mtime).toBe(fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime()); 508 | 509 | await pDelay(1000); 510 | 511 | expect(fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime()).toBeGreaterThan(mtime); 512 | }, 10000); 513 | 514 | it('should set update to a maximum of stale / 2', async () => { 515 | fs.writeFileSync(`${tmpDir}/foo`, ''); 516 | 517 | expect.assertions(2); 518 | 519 | await lockfile.lock(`${tmpDir}/foo`, { update: 6000, stale: 5000 }); 520 | 521 | const mtime = fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime(); 522 | 523 | await pDelay(2000); 524 | 525 | expect(mtime).toBe(fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime()); 526 | 527 | await pDelay(1000); 528 | 529 | expect(fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime()).toBeGreaterThan(mtime); 530 | }, 10000); 531 | 532 | it('should not fail to update mtime when we are over the threshold but mtime is ours', async () => { 533 | fs.writeFileSync(`${tmpDir}/foo`, ''); 534 | await lockfile.lock(`${tmpDir}/foo`, { update: 1000, stale: 2000 }); 535 | sleep(3000); 536 | await pDelay(5000); 537 | }, 16000); 538 | 539 | it('should call the compromised function when we are over the threshold and mtime is not ours', async () => { 540 | fs.writeFileSync(`${tmpDir}/foo`, ''); 541 | 542 | const deferred = pDefer(); 543 | 544 | const handleCompromised = (err) => { 545 | expect(err.code).toBe('ECOMPROMISED'); 546 | expect(err.message).toMatch('Unable to update lock within the stale threshold'); 547 | 548 | deferred.resolve(); 549 | }; 550 | 551 | await lockfile.lock(`${tmpDir}/foo`, { 552 | update: 1000, 553 | stale: 2000, 554 | onCompromised: handleCompromised, 555 | }); 556 | 557 | const mtime = new Date(Date.now() - 60000); 558 | 559 | fs.utimesSync(`${tmpDir}/foo.lock`, mtime, mtime); 560 | 561 | sleep(3000); 562 | 563 | await deferred.promise; 564 | }, 16000); 565 | 566 | it('should allow millisecond precision mtime', async () => { 567 | fs.writeFileSync(`${tmpDir}/foo`, ''); 568 | 569 | const customFs = { 570 | ...fs, 571 | stat(path, cb) { 572 | fs.stat(path, (err, stat) => { 573 | if (err) { 574 | return cb(err); 575 | } 576 | 577 | stat.mtime = new Date((Math.floor(stat.mtime.getTime() / 1000) * 1000) + 123); 578 | cb(null, stat); 579 | }); 580 | }, 581 | }; 582 | 583 | const dateNow = Date.now; 584 | 585 | jest.spyOn(Date, 'now').mockImplementation(() => (Math.floor(dateNow() / 1000) * 1000) + 123); 586 | 587 | await lockfile.lock(`${tmpDir}/foo`, { 588 | fs: customFs, 589 | update: 1000, 590 | }); 591 | 592 | await pDelay(3000); 593 | }); 594 | 595 | it('should allow floor\'ed second precision mtime', async () => { 596 | fs.writeFileSync(`${tmpDir}/foo`, ''); 597 | 598 | const customFs = { 599 | ...fs, 600 | stat(path, cb) { 601 | fs.stat(path, (err, stat) => { 602 | if (err) { 603 | return cb(err); 604 | } 605 | 606 | // Make second precision if not already 607 | stat.mtime = new Date(Math.floor(stat.mtime.getTime() / 1000) * 1000); 608 | cb(null, stat); 609 | }); 610 | }, 611 | }; 612 | 613 | await lockfile.lock(`${tmpDir}/foo`, { 614 | fs: customFs, 615 | update: 1000, 616 | }); 617 | 618 | await pDelay(3000); 619 | }); 620 | 621 | it('should allow ceil\'ed second precision mtime', async () => { 622 | fs.writeFileSync(`${tmpDir}/foo`, ''); 623 | 624 | const customFs = { 625 | ...fs, 626 | stat(path, cb) { 627 | fs.stat(path, (err, stat) => { 628 | if (err) { 629 | return cb(err); 630 | } 631 | 632 | // Make second precision if not already 633 | stat.mtime = new Date(Math.ceil(stat.mtime.getTime() / 1000) * 1000); 634 | cb(null, stat); 635 | }); 636 | }, 637 | }; 638 | 639 | await lockfile.lock(`${tmpDir}/foo`, { 640 | fs: customFs, 641 | update: 1000, 642 | }); 643 | 644 | await pDelay(3000); 645 | }); 646 | -------------------------------------------------------------------------------- /test/misc.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const execa = require('execa'); 5 | const mkdirp = require('mkdirp'); 6 | const rimraf = require('rimraf'); 7 | 8 | const tmpDir = `${__dirname}/tmp`; 9 | 10 | beforeAll(() => mkdirp.sync(tmpDir)); 11 | 12 | afterAll(() => rimraf.sync(tmpDir)); 13 | 14 | afterEach(() => rimraf.sync(`${tmpDir}/*`)); 15 | 16 | it('should always use `options.fs` when calling `fs` methods', () => { 17 | const lockfileContents = fs.readFileSync(`${__dirname}/../lib/lockfile.js`); 18 | 19 | expect(/\s{1,}fs\.[a-z]+/i.test(lockfileContents)).toBe(false); 20 | }); 21 | 22 | it('should remove open locks if the process crashes', async () => { 23 | const { stderr } = await execa('node', [`${__dirname}/fixtures/crash.js`], { reject: false }); 24 | 25 | expect(stderr).toMatch('intencional crash'); 26 | expect(fs.existsSync(`${tmpDir}/foo.lock`)).toBe(false); 27 | }); 28 | 29 | it('should not hold the process if it has no more work to do', async () => { 30 | await execa('node', [`${__dirname}/fixtures/unref.js`]); 31 | }); 32 | 33 | it('should work on stress conditions', async () => { 34 | try { 35 | await execa('node', [`${__dirname}/fixtures/stress.js`]); 36 | } catch (err) { 37 | const stdout = err.stdout || ''; 38 | 39 | if (process.env.CI) { 40 | process.stdout.write(stdout); 41 | } else { 42 | fs.writeFileSync(`${__dirname}/stress.log`, stdout); 43 | } 44 | 45 | throw err; 46 | } 47 | }, 80000); 48 | -------------------------------------------------------------------------------- /test/release.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | const mkdirp = require('mkdirp'); 5 | const rimraf = require('rimraf'); 6 | const lockfile = require('../'); 7 | const unlockAll = require('./util/unlockAll'); 8 | 9 | const tmpDir = `${__dirname}/tmp`; 10 | 11 | beforeAll(() => mkdirp.sync(tmpDir)); 12 | 13 | afterAll(() => rimraf.sync(tmpDir)); 14 | 15 | afterEach(async () => { 16 | await unlockAll(); 17 | rimraf.sync(`${tmpDir}/*`); 18 | }); 19 | 20 | it('should release the lock', async () => { 21 | fs.writeFileSync(`${tmpDir}/foo`, ''); 22 | 23 | const release = await lockfile.lock(`${tmpDir}/foo`); 24 | 25 | await release(); 26 | 27 | await lockfile.lock(`${tmpDir}/foo`); 28 | }); 29 | 30 | it('should remove the lockfile', async () => { 31 | fs.writeFileSync(`${tmpDir}/foo`, ''); 32 | 33 | const release = await lockfile.lock(`${tmpDir}/foo`); 34 | 35 | await release(); 36 | 37 | expect(fs.existsSync(`${tmpDir}/foo.lock`)).toBe(false); 38 | }); 39 | 40 | it('should fail when releasing twice', async () => { 41 | fs.writeFileSync(`${tmpDir}/foo`, ''); 42 | 43 | expect.assertions(1); 44 | 45 | const release = await lockfile.lock(`${tmpDir}/foo`); 46 | 47 | await release(); 48 | 49 | try { 50 | await release(); 51 | } catch (err) { 52 | expect(err.code).toBe('ERELEASED'); 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /test/sync.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | const mkdirp = require('mkdirp'); 5 | const rimraf = require('rimraf'); 6 | const lockfile = require('../'); 7 | const unlockAll = require('./util/unlockAll'); 8 | 9 | const tmpDir = `${__dirname}/tmp`; 10 | 11 | beforeAll(() => mkdirp.sync(tmpDir)); 12 | 13 | afterAll(() => rimraf.sync(tmpDir)); 14 | 15 | afterEach(async () => { 16 | await unlockAll(); 17 | rimraf.sync(`${tmpDir}/*`); 18 | }); 19 | 20 | describe('.lockSync()', () => { 21 | it('should expose a working lockSync', () => { 22 | fs.writeFileSync(`${tmpDir}/foo`, ''); 23 | 24 | const release = lockfile.lockSync(`${tmpDir}/foo`); 25 | 26 | expect(typeof release).toBe('function'); 27 | expect(fs.existsSync(`${tmpDir}/foo.lock`)).toBe(true); 28 | 29 | release(); 30 | 31 | expect(fs.existsSync(`${tmpDir}/foo.lock`)).toBe(false); 32 | }); 33 | 34 | it('should fail if the lock is already acquired', () => { 35 | fs.writeFileSync(`${tmpDir}/foo`, ''); 36 | 37 | lockfile.lockSync(`${tmpDir}/foo`); 38 | 39 | expect(fs.existsSync(`${tmpDir}/foo.lock`)).toBe(true); 40 | expect(() => lockfile.lockSync(`${tmpDir}/foo`)).toThrow(/already being held/); 41 | }); 42 | 43 | it('should pass options correctly', () => { 44 | expect(() => lockfile.lockSync(`${tmpDir}/foo`, { realpath: false })).not.toThrow(); 45 | }); 46 | 47 | it('should not allow retries to be passed', () => { 48 | fs.writeFileSync(`${tmpDir}/foo`, ''); 49 | 50 | expect(() => lockfile.lockSync(`${tmpDir}/foo`, { retries: 10 })).toThrow(/Cannot use retries/i); 51 | 52 | expect(() => lockfile.lockSync(`${tmpDir}/foo`, { retries: { retries: 10 } })).toThrow(/Cannot use retries/i); 53 | 54 | expect(() => { 55 | const release = lockfile.lockSync(`${tmpDir}/foo`, { retries: 0 }); 56 | 57 | release(); 58 | }).not.toThrow(); 59 | 60 | expect(() => { 61 | const release = lockfile.lockSync(`${tmpDir}/foo`, { retries: { retries: 0 } }); 62 | 63 | release(); 64 | }).not.toThrow(); 65 | }); 66 | 67 | it('should fail syncronously if release throws', () => { 68 | fs.writeFileSync(`${tmpDir}/foo`, ''); 69 | 70 | expect.assertions(1); 71 | 72 | const release = lockfile.lockSync(`${tmpDir}/foo`); 73 | 74 | release(); 75 | 76 | expect(() => release()).toThrow('Lock is already released'); 77 | }); 78 | }); 79 | 80 | describe('.unlockSync()', () => { 81 | it('should expose a working unlockSync', () => { 82 | fs.writeFileSync(`${tmpDir}/foo`, ''); 83 | 84 | lockfile.lockSync(`${tmpDir}/foo`); 85 | 86 | expect(fs.existsSync(`${tmpDir}/foo.lock`)).toBe(true); 87 | 88 | lockfile.unlockSync(`${tmpDir}/foo`); 89 | 90 | expect(fs.existsSync(`${tmpDir}/foo.lock`)).toBe(false); 91 | }); 92 | 93 | it('should fail is lock is not acquired', () => { 94 | fs.writeFileSync(`${tmpDir}/foo`, ''); 95 | 96 | expect(() => lockfile.unlockSync(`${tmpDir}/foo`)).toThrow(/not acquired\/owned by you/); 97 | }); 98 | 99 | it('should pass options correctly', () => { 100 | expect(() => lockfile.unlockSync(`${tmpDir}/foo`, { realpath: false })).toThrow(/not acquired\/owned by you/); 101 | }); 102 | }); 103 | 104 | describe('.checkSync()', () => { 105 | it('should expose a working checkSync', () => { 106 | fs.writeFileSync(`${tmpDir}/foo`, ''); 107 | 108 | expect(lockfile.checkSync(`${tmpDir}/foo`)).toBe(false); 109 | 110 | const release = lockfile.lockSync(`${tmpDir}/foo`); 111 | 112 | expect(lockfile.checkSync(`${tmpDir}/foo`)).toBe(true); 113 | 114 | release(); 115 | 116 | expect(lockfile.checkSync(`${tmpDir}/foo`)).toBe(false); 117 | }); 118 | 119 | it('should fail is file does not exist', () => { 120 | expect(() => lockfile.checkSync(`${tmpDir}/some-file-that-will-never-exist`)).toThrow(/ENOENT/); 121 | }); 122 | 123 | it('should pass options correctly', () => { 124 | expect(() => lockfile.checkSync(`${tmpDir}/foo`, { realpath: false })).not.toThrow(); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/unlock.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | const mkdirp = require('mkdirp'); 5 | const rimraf = require('rimraf'); 6 | const pDelay = require('delay'); 7 | const clearTimeouts = require('@segment/clear-timeouts'); 8 | const lockfile = require('../'); 9 | const unlockAll = require('./util/unlockAll'); 10 | 11 | const tmpDir = `${__dirname}/tmp`; 12 | 13 | clearTimeouts.install(); 14 | 15 | beforeAll(() => mkdirp.sync(tmpDir)); 16 | 17 | afterAll(() => rimraf.sync(tmpDir)); 18 | 19 | afterEach(async () => { 20 | clearTimeouts(); 21 | 22 | await unlockAll(); 23 | rimraf.sync(`${tmpDir}/*`); 24 | }); 25 | 26 | it('should fail if the lock is not acquired', async () => { 27 | fs.writeFileSync(`${tmpDir}/foo`, ''); 28 | 29 | expect.assertions(1); 30 | 31 | try { 32 | await lockfile.unlock(`${tmpDir}/foo`); 33 | } catch (err) { 34 | expect(err.code).toBe('ENOTACQUIRED'); 35 | } 36 | }); 37 | 38 | it('should return a promise', async () => { 39 | fs.writeFileSync(`${tmpDir}/foo`, ''); 40 | 41 | const promise = lockfile.unlock(`${tmpDir}/foo`); 42 | 43 | expect(typeof promise.then).toBe('function'); 44 | 45 | await promise.catch(() => {}); 46 | }); 47 | 48 | it('should release the lock', async () => { 49 | fs.writeFileSync(`${tmpDir}/foo`, ''); 50 | 51 | await lockfile.lock(`${tmpDir}/foo`); 52 | 53 | await lockfile.unlock(`${tmpDir}/foo`); 54 | 55 | await lockfile.lock(`${tmpDir}/foo`); 56 | }); 57 | 58 | it('should remove the lockfile', async () => { 59 | fs.writeFileSync(`${tmpDir}/foo`, ''); 60 | 61 | await lockfile.lock(`${tmpDir}/foo`); 62 | 63 | await lockfile.unlock(`${tmpDir}/foo`); 64 | 65 | expect(fs.existsSync(`${tmpDir}/foo.lock`)).toBe(false); 66 | }); 67 | 68 | it('should fail if removing the lockfile errors out', async () => { 69 | fs.writeFileSync(`${tmpDir}/foo`, ''); 70 | 71 | const customFs = { 72 | ...fs, 73 | rmdir: (path, callback) => callback(new Error('foo')), 74 | }; 75 | 76 | expect.assertions(1); 77 | 78 | await lockfile.lock(`${tmpDir}/foo`); 79 | 80 | try { 81 | await lockfile.unlock(`${tmpDir}/foo`, { fs: customFs }); 82 | } catch (err) { 83 | expect(err.message).toBe('foo'); 84 | } 85 | }); 86 | 87 | it('should ignore ENOENT errors when removing the lockfile', async () => { 88 | fs.writeFileSync(`${tmpDir}/foo`, ''); 89 | 90 | const customFs = { 91 | ...fs, 92 | rmdir: jest.fn((path, callback) => callback(Object.assign(new Error(), { code: 'ENOENT' }))), 93 | }; 94 | 95 | await lockfile.lock(`${tmpDir}/foo`); 96 | 97 | await lockfile.unlock(`${tmpDir}/foo`, { fs: customFs }); 98 | 99 | expect(customFs.rmdir).toHaveBeenCalledTimes(1); 100 | }); 101 | 102 | it('should stop updating the lockfile mtime', async () => { 103 | fs.writeFileSync(`${tmpDir}/foo`, ''); 104 | 105 | const customFs = { ...fs }; 106 | 107 | await lockfile.lock(`${tmpDir}/foo`, { update: 2000, fs: customFs }); 108 | 109 | customFs.utimes = jest.fn((path, atime, mtime, callback) => callback()); 110 | 111 | await lockfile.unlock(`${tmpDir}/foo`); 112 | 113 | // First update occurs at 2000ms 114 | await pDelay(2500); 115 | 116 | expect(customFs.utimes).toHaveBeenCalledTimes(0); 117 | }, 10000); 118 | 119 | it('should stop updating the lockfile mtime (slow fs)', async () => { 120 | fs.writeFileSync(`${tmpDir}/foo`, ''); 121 | 122 | const customFs = { ...fs }; 123 | 124 | await lockfile.lock(`${tmpDir}/foo`, { fs: customFs, update: 2000 }); 125 | 126 | customFs.utimes = jest.fn((...args) => setTimeout(() => fs.utimes(...args), 2000)); 127 | 128 | await pDelay(3000); 129 | 130 | await lockfile.unlock(`${tmpDir}/foo`); 131 | 132 | await pDelay(3000); 133 | 134 | expect(customFs.utimes).toHaveBeenCalledTimes(1); 135 | }, 10000); 136 | 137 | it('should stop updating the lockfile mtime (slow fs + new lock)', async () => { 138 | fs.writeFileSync(`${tmpDir}/foo`, ''); 139 | 140 | const customFs = { ...fs }; 141 | 142 | await lockfile.lock(`${tmpDir}/foo`, { fs: customFs, update: 2000 }); 143 | 144 | customFs.utimes = jest.fn((...args) => setTimeout(() => fs.utimes(...args), 2000)); 145 | 146 | await pDelay(3000); 147 | 148 | await lockfile.unlock(`${tmpDir}/foo`); 149 | 150 | await lockfile.lock(`${tmpDir}/foo`); 151 | 152 | await pDelay(3000); 153 | 154 | expect(customFs.utimes).toHaveBeenCalledTimes(1); 155 | }, 10000); 156 | 157 | it('should resolve symlinks by default', async () => { 158 | fs.writeFileSync(`${tmpDir}/foo`, ''); 159 | fs.symlinkSync(`${tmpDir}/foo`, `${tmpDir}/bar`); 160 | 161 | await lockfile.lock(`${tmpDir}/foo`); 162 | 163 | await lockfile.unlock(`${tmpDir}/bar`); 164 | 165 | expect(fs.existsSync(`${tmpDir}/foo.lock`)).toBe(false); 166 | }); 167 | 168 | it('should not resolve symlinks if realpath is false', async () => { 169 | fs.writeFileSync(`${tmpDir}/foo`, ''); 170 | fs.symlinkSync(`${tmpDir}/foo`, `${tmpDir}/bar`); 171 | 172 | expect.assertions(1); 173 | 174 | await lockfile.lock(`${tmpDir}/foo`); 175 | 176 | try { 177 | await lockfile.unlock(`${tmpDir}/bar`, { realpath: false }); 178 | } catch (err) { 179 | expect(err.code).toBe('ENOTACQUIRED'); 180 | } 181 | }); 182 | 183 | it('should use a custom fs', async () => { 184 | const customFs = { 185 | ...fs, 186 | realpath: (path, callback) => callback(new Error('foo')), 187 | }; 188 | 189 | expect.assertions(1); 190 | 191 | try { 192 | await lockfile.unlock(`${tmpDir}/foo`, { fs: customFs }); 193 | } catch (err) { 194 | expect(err.message).toBe('foo'); 195 | } 196 | }); 197 | -------------------------------------------------------------------------------- /test/util/unlockAll.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { getLocks } = require('../../lib/lockfile'); 4 | const { unlock } = require('../..'); 5 | 6 | function unlockAll() { 7 | const locks = getLocks(); 8 | const promises = Object.keys(locks).map((file) => unlock(file, { realpath: false })); 9 | 10 | return Promise.all(promises); 11 | } 12 | 13 | module.exports = unlockAll; 14 | --------------------------------------------------------------------------------