123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- 'use strict';
- const path = require('path');
- const fs = require('graceful-fs');
- const retry = require('retry');
- const onExit = require('signal-exit');
- const mtimePrecision = require('./mtime-precision');
- const locks = {};
- function getLockFile(file, options) {
- return options.lockfilePath || `${file}.lock`;
- }
- function resolveCanonicalPath(file, options, callback) {
- if (!options.realpath) {
- return callback(null, path.resolve(file));
- }
- // Use realpath to resolve symlinks
- // It also resolves relative paths
- options.fs.realpath(file, callback);
- }
- function acquireLock(file, options, callback) {
- const lockfilePath = getLockFile(file, options);
- // Use mkdir to create the lockfile (atomic operation)
- options.fs.mkdir(lockfilePath, (err) => {
- if (!err) {
- // At this point, we acquired the lock!
- // Probe the mtime precision
- return mtimePrecision.probe(lockfilePath, options.fs, (err, mtime, mtimePrecision) => {
- // If it failed, try to remove the lock..
- /* istanbul ignore if */
- if (err) {
- options.fs.rmdir(lockfilePath, () => {});
- return callback(err);
- }
- callback(null, mtime, mtimePrecision);
- });
- }
- // 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 held'), { code: 'ELOCKED', file }));
- }
- options.fs.stat(lockfilePath, (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, { ...options, stale: 0 }, callback);
- }
- return callback(err);
- }
- if (!isLockStale(stat, options)) {
- return callback(Object.assign(new Error('Lock file is already being held'), { 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, { ...options, stale: 0 }, callback);
- });
- });
- });
- }
- function isLockStale(stat, options) {
- return stat.mtime.getTime() < Date.now() - options.stale;
- }
- function removeLock(file, options, callback) {
- // Remove lockfile, ignoring ENOENT errors
- options.fs.rmdir(getLockFile(file, options), (err) => {
- if (err && err.code !== 'ENOENT') {
- return callback(err);
- }
- callback();
- });
- }
- function updateLock(file, options) {
- const lock = locks[file];
- // Just for safety, should never happen
- /* istanbul ignore if */
- if (lock.updateTimeout) {
- return;
- }
- lock.updateDelay = lock.updateDelay || options.update;
- lock.updateTimeout = setTimeout(() => {
- lock.updateTimeout = null;
- // Stat the file to check if mtime is still ours
- // If it is, we can still recover from a system sleep or a busy event loop
- options.fs.stat(lock.lockfilePath, (err, stat) => {
- const isOverThreshold = lock.lastUpdate + options.stale < Date.now();
- // If it failed to update the lockfile, keep trying unless
- // the lockfile was deleted or we are over the threshold
- if (err) {
- if (err.code === 'ENOENT' || isOverThreshold) {
- return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' }));
- }
- lock.updateDelay = 1000;
- return updateLock(file, options);
- }
- const isMtimeOurs = lock.mtime.getTime() === stat.mtime.getTime();
- if (!isMtimeOurs) {
- return setLockAsCompromised(
- file,
- lock,
- Object.assign(
- new Error('Unable to update lock within the stale threshold'),
- { code: 'ECOMPROMISED' }
- ));
- }
- const mtime = mtimePrecision.getMtime(lock.mtimePrecision);
- options.fs.utimes(lock.lockfilePath, mtime, mtime, (err) => {
- const isOverThreshold = lock.lastUpdate + options.stale < Date.now();
- // Ignore if the lock was released
- if (lock.released) {
- return;
- }
- // If it failed to update the lockfile, keep trying unless
- // the lockfile was deleted or we are over the threshold
- if (err) {
- if (err.code === 'ENOENT' || isOverThreshold) {
- return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' }));
- }
- lock.updateDelay = 1000;
- return updateLock(file, options);
- }
- // All ok, keep updating..
- lock.mtime = mtime;
- lock.lastUpdate = Date.now();
- 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.
- /* istanbul ignore else */
- if (lock.updateTimeout.unref) {
- lock.updateTimeout.unref();
- }
- }
- function setLockAsCompromised(file, lock, err) {
- // Signal the lock has been released
- lock.released = true;
- // Cancel lock mtime update
- // Just for safety, at this point updateTimeout should be null
- /* istanbul ignore if */
- if (lock.updateTimeout) {
- clearTimeout(lock.updateTimeout);
- }
- if (locks[file] === lock) {
- delete locks[file];
- }
- lock.options.onCompromised(err);
- }
- // ----------------------------------------------------------
- function lock(file, options, callback) {
- /* istanbul ignore next */
- options = {
- stale: 10000,
- update: null,
- realpath: true,
- retries: 0,
- fs,
- onCompromised: (err) => { throw err; },
- ...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);
- // Resolve to a canonical file path
- resolveCanonicalPath(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, mtime, mtimePrecision) => {
- if (operation.retry(err)) {
- return;
- }
- if (err) {
- return callback(operation.mainError());
- }
- // We now own the lock
- const lock = locks[file] = {
- lockfilePath: getLockFile(file, options),
- mtime,
- mtimePrecision,
- options,
- 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, { ...options, realpath: false }, releasedCallback);
- });
- });
- });
- });
- }
- function unlock(file, options, callback) {
- options = {
- fs,
- realpath: true,
- ...options,
- };
- // Resolve to a canonical file path
- resolveCanonicalPath(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 check(file, options, callback) {
- options = {
- stale: 10000,
- realpath: true,
- fs,
- ...options,
- };
- options.stale = Math.max(options.stale || 0, 2000);
- // Resolve to a canonical file path
- resolveCanonicalPath(file, options, (err, file) => {
- if (err) {
- return callback(err);
- }
- // Check if lockfile exists
- options.fs.stat(getLockFile(file, options), (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);
- }
- // Otherwise, check if lock is stale by analyzing the file mtime
- return callback(null, !isLockStale(stat, options));
- });
- });
- }
- function getLocks() {
- return locks;
- }
- // Remove acquired locks on exit
- /* istanbul ignore next */
- onExit(() => {
- for (const file in locks) {
- const options = locks[file].options;
- try { options.fs.rmdirSync(getLockFile(file, options)); } catch (e) { /* Empty */ }
- }
- });
- module.exports.lock = lock;
- module.exports.unlock = unlock;
- module.exports.check = check;
- module.exports.getLocks = getLocks;
|