proper-lockfile
Advanced tools
Comparing version 2.0.1 to 3.0.0
369
index.js
'use strict'; | ||
const fs = require('graceful-fs'); | ||
const path = require('path'); | ||
const retry = require('retry'); | ||
const syncFs = require('./lib/syncFs'); | ||
const lockfile = require('./lib/lockfile'); | ||
const { toPromise, toSync, toSyncOptions } = require('./lib/adapter'); | ||
const locks = {}; | ||
async function lock(file, options) { | ||
const release = await toPromise(lockfile.lock)(file, options); | ||
function getLockFile(file) { | ||
return `${file}.lock`; | ||
return toPromise(release); | ||
} | ||
function canonicalPath(file, options, callback) { | ||
if (!options.realpath) { | ||
return callback(null, path.resolve(file)); | ||
} | ||
function lockSync(file, options) { | ||
const release = toSync(lockfile.lock)(file, toSyncOptions(options)); | ||
// Use realpath to resolve symlinks | ||
// It also resolves relative paths | ||
options.fs.realpath(file, callback); | ||
return toSync(release); | ||
} | ||
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 unlock(file, options) { | ||
return toPromise(lockfile.unlock)(file, options); | ||
} | ||
function isLockStale(stat, options) { | ||
return stat.mtime.getTime() < Date.now() - options.stale; | ||
} | ||
function removeLock(file, options, callback) { | ||
// Remove lockfile, ignoring ENOENT errors | ||
options.fs.rmdir(getLockFile(file), (err) => { | ||
if (err && err.code !== 'ENOENT') { | ||
return callback(err); | ||
} | ||
callback(); | ||
}); | ||
} | ||
function updateLock(file, options) { | ||
const lock = locks[file]; | ||
/* istanbul ignore next */ | ||
if (lock.updateTimeout) { | ||
return; | ||
} | ||
lock.updateDelay = lock.updateDelay || options.update; | ||
lock.updateTimeout = setTimeout(() => { | ||
const mtime = Date.now() / 1000; | ||
lock.updateTimeout = null; | ||
options.fs.utimes(getLockFile(file), mtime, mtime, (err) => { | ||
// Ignore if the lock was released | ||
if (lock.released) { | ||
return; | ||
} | ||
// Verify if we are within the stale threshold | ||
if (lock.lastUpdate <= Date.now() - options.stale && | ||
lock.lastUpdate > Date.now() - options.stale * 2) { | ||
return compromisedLock(file, lock, Object.assign(new Error(lock.updateError || 'Unable to update lock within the stale \ | ||
threshold'), { code: 'ECOMPROMISED' })); | ||
} | ||
// If the file is older than (stale * 2), we assume the clock is moved manually, | ||
// which we consider a valid case | ||
// If it failed to update the lockfile, keep trying unless | ||
// the lockfile was deleted! | ||
if (err) { | ||
if (err.code === 'ENOENT') { | ||
return compromisedLock(file, lock, Object.assign(err, { code: 'ECOMPROMISED' })); | ||
} | ||
lock.updateError = err; | ||
lock.updateDelay = 1000; | ||
return updateLock(file, options); | ||
} | ||
// All ok, keep updating.. | ||
lock.lastUpdate = Date.now(); | ||
lock.updateError = null; | ||
lock.updateDelay = null; | ||
updateLock(file, options); | ||
}); | ||
}, lock.updateDelay); | ||
// Unref the timer so that the nodejs process can exit freely | ||
// This is safe because all acquired locks will be automatically released | ||
// on process exit | ||
// We first check that `lock.updateTimeout.unref` exists because some users | ||
// may be using this module outside of NodeJS (e.g., in an electron app), | ||
// and in those cases `setTimeout` return an integer. | ||
if (lock.updateTimeout.unref) { | ||
lock.updateTimeout.unref(); | ||
} | ||
} | ||
function compromisedLock(file, lock, err) { | ||
lock.released = true; // Signal the lock has been released | ||
/* istanbul ignore next */ | ||
lock.updateTimeout && clearTimeout(lock.updateTimeout); // Cancel lock mtime update | ||
if (locks[file] === lock) { | ||
delete locks[file]; | ||
} | ||
lock.compromised(err); | ||
} | ||
// ----------------------------------------- | ||
function lock(file, options, compromised, callback) { | ||
if (typeof options === 'function') { | ||
callback = compromised; | ||
compromised = options; | ||
options = null; | ||
} | ||
if (!callback) { | ||
callback = compromised; | ||
compromised = null; | ||
} | ||
options = Object.assign({ | ||
stale: 10000, | ||
update: null, | ||
realpath: true, | ||
retries: 0, | ||
fs, | ||
}, options); | ||
options.retries = options.retries || 0; | ||
options.retries = typeof options.retries === 'number' ? { retries: options.retries } : options.retries; | ||
options.stale = Math.max(options.stale || 0, 2000); | ||
options.update = options.update == null ? options.stale / 2 : options.update || 0; | ||
options.update = Math.max(Math.min(options.update, options.stale / 2), 1000); | ||
compromised = compromised || function (err) { throw err; }; | ||
// Resolve to a canonical file path | ||
canonicalPath(file, options, (err, file) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
// Attempt to acquire the lock | ||
const operation = retry.operation(options.retries); | ||
operation.attempt(() => { | ||
acquireLock(file, options, (err) => { | ||
if (operation.retry(err)) { | ||
return; | ||
} | ||
if (err) { | ||
return callback(operation.mainError()); | ||
} | ||
// We now own the lock | ||
const lock = locks[file] = { | ||
options, | ||
compromised, | ||
lastUpdate: Date.now(), | ||
}; | ||
// We must keep the lock fresh to avoid staleness | ||
updateLock(file, options); | ||
callback(null, (releasedCallback) => { | ||
if (lock.released) { | ||
return releasedCallback && | ||
releasedCallback(Object.assign(new Error('Lock is already released'), { code: 'ERELEASED' })); | ||
} | ||
// Not necessary to use realpath twice when unlocking | ||
unlock(file, Object.assign({}, options, { realpath: false }), releasedCallback); | ||
}); | ||
}); | ||
}); | ||
}); | ||
} | ||
function unlock(file, options, callback) { | ||
if (typeof options === 'function') { | ||
callback = options; | ||
options = null; | ||
} | ||
options = Object.assign({ | ||
fs, | ||
realpath: true, | ||
}, options); | ||
callback = callback || function () {}; | ||
// Resolve to a canonical file path | ||
canonicalPath(file, options, (err, file) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
// Skip if the lock is not acquired | ||
const lock = locks[file]; | ||
if (!lock) { | ||
return callback(Object.assign(new Error('Lock is not acquired/owned by you'), { code: 'ENOTACQUIRED' })); | ||
} | ||
lock.updateTimeout && clearTimeout(lock.updateTimeout); // Cancel lock mtime update | ||
lock.released = true; // Signal the lock has been released | ||
delete locks[file]; // Delete from locks | ||
removeLock(file, options, callback); | ||
}); | ||
} | ||
function lockSync(file, options, compromised) { | ||
if (typeof options === 'function') { | ||
compromised = options; | ||
options = null; | ||
} | ||
options = options || {}; | ||
options.fs = syncFs(options.fs || fs); | ||
options.retries = options.retries || 0; | ||
options.retries = typeof options.retries === 'number' ? { retries: options.retries } : options.retries; | ||
// Retries are not allowed because it requires the flow to be sync | ||
if (options.retries.retries) { | ||
throw Object.assign(new Error('Cannot use retries with the sync api'), { code: 'ESYNC' }); | ||
} | ||
let err; | ||
let release; | ||
lock(file, options, compromised, (_err, _release) => { | ||
err = _err; | ||
release = _release; | ||
}); | ||
if (err) { | ||
throw err; | ||
} | ||
return release; | ||
} | ||
function unlockSync(file, options) { | ||
options = options || {}; | ||
options.fs = syncFs(options.fs || fs); | ||
let err; | ||
unlock(file, options, (_err) => { | ||
err = _err; | ||
}); | ||
if (err) { | ||
throw err; | ||
} | ||
return toSync(lockfile.unlock)(file, toSyncOptions(options)); | ||
} | ||
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 check(file, options) { | ||
return toPromise(lockfile.check)(file, 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; | ||
return toSync(lockfile.check)(file, toSyncOptions(options)); | ||
} | ||
// 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; | ||
@@ -376,0 +35,0 @@ module.exports.unlock = unlock; |
{ | ||
"name": "proper-lockfile", | ||
"version": "2.0.1", | ||
"description": "A inter-process and inter-machine lockfile utility that works on a local or network file system.", | ||
"main": "index.js", | ||
"scripts": { | ||
"lint": "eslint '{*.js,lib/**/*.js,test/**/*.js}' --ignore-pattern=test/coverage", | ||
"test": "mocha", | ||
"test-cov": "istanbul cover --dir test/coverage _mocha && echo open test/coverage/lcov-report/index.html", | ||
"test-travis": "istanbul cover _mocha --report lcovonly && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/IndigoUnited/node-proper-lockfile/issues/" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/IndigoUnited/node-proper-lockfile.git" | ||
}, | ||
"version": "3.0.0", | ||
"description": "A inter-process and inter-machine lockfile utility that works on a local or network file system", | ||
"keywords": [ | ||
@@ -25,28 +11,59 @@ "lock", | ||
"fs", | ||
"rename", | ||
"cross", | ||
"machine" | ||
"cross-process" | ||
], | ||
"author": "IndigoUnited <hello@indigounited.com> (http://indigounited.com)", | ||
"author": "André Cruz <andre@moxy.studio>", | ||
"homepage": "https://github.com/moxystudio/node-promptly", | ||
"repository": { | ||
"type": "git", | ||
"url": "git@github.com:moxystudio/node-promptly.git" | ||
}, | ||
"license": "MIT", | ||
"main": "index.js", | ||
"files": [ | ||
"lib" | ||
], | ||
"scripts": { | ||
"lint": "eslint .", | ||
"test": "jest --env node --coverage --runInBand", | ||
"prerelease": "npm t && npm run lint", | ||
"release": "standard-version", | ||
"precommit": "lint-staged", | ||
"commitmsg": "commitlint -e $GIT_PARAMS" | ||
}, | ||
"standard-version": { | ||
"scripts": { | ||
"posttag": "git push --follow-tags origin master && npm publish" | ||
} | ||
}, | ||
"lint-staged": { | ||
"*.js": [ | ||
"eslint --fix", | ||
"git add" | ||
] | ||
}, | ||
"commitlint": { | ||
"extends": [ | ||
"@commitlint/config-conventional" | ||
] | ||
}, | ||
"dependencies": { | ||
"graceful-fs": "^4.1.2", | ||
"retry": "^0.10.0" | ||
"graceful-fs": "^4.1.11", | ||
"retry": "^0.10.1" | ||
}, | ||
"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" | ||
"@commitlint/cli": "^6.0.0", | ||
"@commitlint/config-conventional": "^6.0.2", | ||
"@segment/clear-timeouts": "^2.0.0", | ||
"delay": "^2.0.0", | ||
"eslint": "^4.3.0", | ||
"eslint-config-moxy": "^4.1.0", | ||
"husky": "^0.14.3", | ||
"jest": "^22.0.0", | ||
"lint-staged": "^6.0.0", | ||
"mkdirp": "^0.5.1", | ||
"p-defer": "^1.0.0", | ||
"rimraf": "^2.6.2", | ||
"stable": "^0.1.6", | ||
"standard-version": "^4.2.0" | ||
} | ||
} |
# 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 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] [![Greenkeeper badge][greenkeeper-image]][greenkeeper-url] | ||
@@ -8,10 +8,12 @@ [npm-url]:https://npmjs.org/package/proper-lockfile | ||
[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 | ||
[travis-url]:https://travis-ci.org/moxystudio/node-proper-lockfile | ||
[travis-image]:http://img.shields.io/travis/moxystudio/node-proper-lockfile/master.svg | ||
[codecov-url]:https://codecov.io/gh/moxystudio/node-proper-lockfile | ||
[codecov-image]:https://img.shields.io/codecov/c/github/moxystudio/node-proper-lockfile/master.svg | ||
[david-dm-url]:https://david-dm.org/moxystudio/node-proper-lockfile | ||
[david-dm-image]:https://img.shields.io/david/moxystudio/node-proper-lockfile.svg | ||
[david-dm-dev-url]:https://david-dm.org/moxystudio/node-proper-lockfile?type=dev | ||
[david-dm-dev-image]:https://img.shields.io/david/dev/moxystudio/node-proper-lockfile.svg | ||
[greenkeeper-image]:https://badges.greenkeeper.io/moxystudio/node-proper-lockfile.svg | ||
[greenkeeper-url]:https://greenkeeper.io/ | ||
@@ -67,8 +69,7 @@ An inter-process and inter-machine lockfile utility that works on a local or network file system. | ||
### .lock(file, [options], [compromised], callback) | ||
### .lock(file, [options]) | ||
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. | ||
If the lock succeeds, a `release` function is provided that should be called when you want to release the lock. | ||
@@ -82,2 +83,3 @@ Available options: | ||
- `fs`: A custom fs to use, defaults to `graceful-fs` | ||
- `onCompromised`: Called if the lock gets compromised, defaults to a function that simply throws the error which will probably cause the process to die | ||
@@ -88,21 +90,9 @@ | ||
lockfile.lock('some/file', (err, release) => { | ||
if (err) { | ||
throw err; // Lock failed | ||
} | ||
lockfile.lock('some/file') | ||
.then((release) => { | ||
// Do something while the file is locked | ||
// Call the provided release function when you're done | ||
release(); | ||
// Note that you can optionally handle release errors | ||
// Though it's not mandatory since it will eventually stale | ||
/*release((err) => { | ||
// At this point the lock was effectively released or an error | ||
// occurred while removing it | ||
if (err) { | ||
throw err; | ||
} | ||
});*/ | ||
// Call the provided release function when you're done, | ||
// which will also return a promise | ||
return release(); | ||
}); | ||
@@ -112,3 +102,3 @@ ``` | ||
### .unlock(file, [options], [callback]) | ||
### .unlock(file, [options]) | ||
@@ -119,4 +109,2 @@ Releases a previously acquired lock on `file`. | ||
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: | ||
@@ -131,24 +119,14 @@ | ||
lockfile.lock('some/file', (err) => { | ||
if (err) { | ||
throw err; | ||
} | ||
lockfile.lock('some/file') | ||
.then(() => { | ||
// Do something while the file is locked | ||
// Later.. | ||
lockfile.unlock('some/file'); | ||
// or.. | ||
/*lockfile.unlock('some/file', (err) => { | ||
// At this point the lock was effectively released or an error | ||
// occurred while removing it | ||
if (err) { | ||
throw err; | ||
} | ||
});*/ | ||
return lockfile.unlock('some/file'); | ||
}); | ||
``` | ||
### .check(file, [options], callback) | ||
### .check(file, [options]) | ||
Check if the file is locked and its lockfile is not stale. Callback is called with callback(error, isLocked). | ||
Check if the file is locked and its lockfile is not stale. | ||
@@ -165,7 +143,4 @@ Available options: | ||
lockfile.check('some/file', (err, isLocked) => { | ||
if (err) { | ||
throw err; | ||
} | ||
lockfile.check('some/file') | ||
.then((isLocked) => { | ||
// isLocked will be true if 'some/file' is locked, false otherwise | ||
@@ -175,3 +150,3 @@ }); | ||
### .lockSync(file, [options], [compromised]) | ||
### .lockSync(file, [options]) | ||
@@ -181,3 +156,2 @@ Sync version of `.lock()`. | ||
### .unlockSync(file, [options]) | ||
@@ -213,3 +187,3 @@ | ||
`$ npm test` | ||
`$ npm test-cov` to get coverage report | ||
`$ npm test -- --watch` during development | ||
@@ -216,0 +190,0 @@ The test suite is very extensive. There's even a stress test to guarantee exclusiveness of locks. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
22669
7
340
0
14
1
185
Updatedgraceful-fs@^4.1.11
Updatedretry@^0.10.1