Update:Remove proper-lockfile dependency

This commit is contained in:
advplyr 2022-06-07 20:15:00 -05:00
parent b7e546f2f5
commit e06a015d6e
17 changed files with 1038 additions and 49 deletions

View file

@ -0,0 +1,85 @@
'use strict';
const fs = require('graceful-fs');
function createSyncFs(fs) {
const methods = ['mkdir', 'realpath', 'stat', 'rmdir', 'utimes'];
const newFs = { ...fs };
methods.forEach((method) => {
newFs[method] = (...args) => {
const callback = args.pop();
let ret;
try {
ret = fs[`${method}Sync`](...args);
} catch (err) {
return callback(err);
}
callback(null, ret);
};
});
return newFs;
}
// ----------------------------------------------------------
function toPromise(method) {
return (...args) => new Promise((resolve, reject) => {
args.push((err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
method(...args);
});
}
function toSync(method) {
return (...args) => {
let err;
let result;
args.push((_err, _result) => {
err = _err;
result = _result;
});
method(...args);
if (err) {
throw err;
}
return result;
};
}
function toSyncOptions(options) {
// Shallow clone options because we are oging to mutate them
options = { ...options };
// Transform fs to use the sync methods instead
options.fs = createSyncFs(options.fs || fs);
// Retries are not allowed because it requires the flow to be sync
if (
(typeof options.retries === 'number' && options.retries > 0) ||
(options.retries && typeof options.retries.retries === 'number' && options.retries.retries > 0)
) {
throw Object.assign(new Error('Cannot use retries with the sync api'), { code: 'ESYNC' });
}
return options;
}
module.exports = {
toPromise,
toSync,
toSyncOptions,
};

View file

@ -0,0 +1,342 @@
'use strict';
const path = require('path');
const fs = require('graceful-fs');
const retry = require('../../retry');
const onExit = require('../../signalExit');
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;

View file

@ -0,0 +1,55 @@
'use strict';
const cacheSymbol = Symbol();
function probe(file, fs, callback) {
const cachedPrecision = fs[cacheSymbol];
if (cachedPrecision) {
return fs.stat(file, (err, stat) => {
/* istanbul ignore if */
if (err) {
return callback(err);
}
callback(null, stat.mtime, cachedPrecision);
});
}
// Set mtime by ceiling Date.now() to seconds + 5ms so that it's "not on the second"
const mtime = new Date((Math.ceil(Date.now() / 1000) * 1000) + 5);
fs.utimes(file, mtime, mtime, (err) => {
/* istanbul ignore if */
if (err) {
return callback(err);
}
fs.stat(file, (err, stat) => {
/* istanbul ignore if */
if (err) {
return callback(err);
}
const precision = stat.mtime.getTime() % 1000 === 0 ? 's' : 'ms';
// Cache the precision in a non-enumerable way
Object.defineProperty(fs, cacheSymbol, { value: precision });
callback(null, stat.mtime, precision);
});
});
}
function getMtime(precision) {
let now = Date.now();
if (precision === 's') {
now = Math.ceil(now / 1000) * 1000;
}
return new Date(now);
}
module.exports.probe = probe;
module.exports.getMtime = getMtime;