rwlockfile
Version:
lockfile utility with reader/writers
339 lines (338 loc) • 9.54 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const path = require("path");
const errors_1 = require("./errors");
const decorators_1 = require("./decorators");
const version = require('../package.json').version;
class Lockfile {
/**
* creates a new simple lockfile without read/write support
*/
constructor(base, options = {}) {
this.timeout = 30000;
this.retryInterval = 10;
this.stale = 10000;
this._count = 0;
this.timeout = options.timeout || this.timeout;
this.retryInterval = options.retryInterval || this.retryInterval;
this.base = base;
this._debug = options.debug;
this.fs = require('fs-extra');
this.uuid = require('uuid/v4')();
instances.push(this);
}
get count() {
return this._count;
}
get dirPath() {
return path.resolve(this.base + '.lock');
}
/**
* creates a lock
* same as add
*/
lock() {
return this.add();
}
/**
* creates a lock
* same as add
*/
lockSync() {
this.addSync();
}
/**
* removes all lock counts
*/
async unlock() {
if (!this.count)
return;
this._debugReport('unlock');
await this.fs.remove(this.dirPath);
await this.fs.remove(this._infoPath);
this._stopLocking();
}
/**
* removes all lock counts
*/
unlockSync() {
if (!this.count)
return;
this._debugReport('unlock');
this.fs.removeSync(this.dirPath);
this.fs.removeSync(this._infoPath);
this._stopLocking();
}
/**
* adds 1 lock count
*/
async add(opts = {}) {
this._debugReport('add', opts.reason);
if (!this.count)
await this._add(opts);
this._count++;
}
/**
* adds 1 lock count
*/
addSync(opts = {}) {
this._debugReport('add', opts.reason);
if (!this.count)
this._lockSync(opts);
this._count++;
}
/**
* removes 1 lock count
*/
async remove() {
this._debugReport('remove');
switch (this.count) {
case 0:
break;
case 1:
await this.unlock();
break;
default:
this._count--;
break;
}
}
/**
* removes 1 lock count
*/
removeSync() {
switch (this.count) {
case 0:
break;
case 1:
this.unlockSync();
break;
default:
this._count--;
break;
}
}
/**
* check if this instance can get a lock
* returns true if it already has a lock
*/
async check() {
const mtime = await this.fetchMtime();
const status = this._status(mtime);
return ['open', 'have_lock', 'stale'].includes(status);
}
/**
* check if this instance can get a lock
* returns true if it already has a lock
*/
checkSync() {
const mtime = this.fetchMtimeSync();
const status = this._status(mtime);
return ['open', 'have_lock', 'stale'].includes(status);
}
get _infoPath() {
return path.resolve(this.dirPath + '.info.json');
}
async fetchReason() {
try {
const b = await this.fs.readJSON(this._infoPath);
return b.reason;
}
catch (err) {
if (err.code !== 'ENOENT')
this.debug(err);
}
}
fetchReasonSync() {
try {
const b = this.fs.readJSONSync(this._infoPath);
return b.reason;
}
catch (err) {
if (err.code !== 'ENOENT')
this.debug(err);
}
}
async _saveReason(reason) {
try {
await this.fs.writeJSON(this._infoPath, {
version,
uuid: this.uuid,
pid: process.pid,
reason,
});
}
catch (err) {
this.debug(err);
}
}
_saveReasonSync(reason) {
try {
this.fs.writeJSONSync(this._infoPath, {
version,
uuid: this.uuid,
pid: process.pid,
reason,
});
}
catch (err) {
this.debug(err);
}
}
async fetchMtime() {
try {
const { mtime } = await this.fs.stat(this.dirPath);
return mtime;
}
catch (err) {
if (err.code !== 'ENOENT')
throw err;
}
}
fetchMtimeSync() {
try {
const { mtime } = this.fs.statSync(this.dirPath);
return mtime;
}
catch (err) {
if (err.code !== 'ENOENT')
throw err;
}
}
isStale(mtime) {
if (!mtime)
return true;
return mtime < new Date(Date.now() - this.stale);
}
debug(msg, ...args) {
if (this._debug)
this._debug(msg, ...args);
}
async _add(opts) {
await this._lock(Object.assign({ timeout: this.timeout, retryInterval: this.retryInterval, ifLocked: ({ reason: _ }) => { } }, opts));
await this._saveReason(opts.reason);
this.startLocking();
}
async _lock(opts) {
this.debug('_lock', this.dirPath);
await this.fs.mkdirp(path.dirname(this.dirPath));
try {
await this.fs.mkdir(this.dirPath);
}
catch (err) {
if (!['EEXIST', 'EPERM'].includes(err.code))
throw err;
// grab reason
const reason = await this.fetchReason();
this.debug('waiting for lock', reason, this.dirPath);
// run callback
await opts.ifLocked({ reason });
// check if timed out
if (opts.timeout < 0)
throw new errors_1.LockfileError({ reason, file: this.dirPath });
// check if stale
const mtime = await this.fetchMtime();
const status = this._status(mtime);
switch (status) {
case 'stale':
try {
await this.fs.rmdir(this.dirPath);
}
catch (err) {
if (err.code !== 'ENOENT')
throw err;
}
case 'open':
case 'have_lock':
return this._lock(opts);
}
// wait before retrying
const interval = random(opts.retryInterval / 2, opts.retryInterval * 2);
await wait(interval);
return this._lock(Object.assign({}, opts, { timeout: opts.timeout - interval, retryInterval: opts.retryInterval * 2 }));
}
}
_lockSync({ reason, retries = 20 } = {}) {
this.debug('_lockSync', this.dirPath);
this.fs.mkdirpSync(path.dirname(this.dirPath));
try {
this.fs.mkdirSync(this.dirPath);
}
catch (err) {
if (!['EEXIST', 'EPERM'].includes(err.code))
throw err;
// check if stale
const mtime = this.fetchMtimeSync();
const status = this._status(mtime);
if (retries <= 0) {
let reason = this.fetchReasonSync();
throw new errors_1.LockfileError({ reason, file: this.dirPath });
}
if (status === 'stale') {
try {
this.fs.rmdirSync(this.dirPath);
}
catch (err) {
if (!['EPERM', 'ENOENT'].includes(err.code))
throw err;
}
}
return this._lockSync({ reason, retries: retries - 1 });
}
this._saveReasonSync(reason);
this.startLocking();
}
_status(mtime) {
if (this.count)
return 'have_lock';
if (!mtime)
return 'open';
const stale = this.isStale(mtime);
if (mtime && stale)
return 'stale';
return 'locked';
}
startLocking() {
this.updater = setInterval(() => {
let now = Date.now() / 1000;
this.fs.utimes(this.dirPath, now, now).catch(err => {
this.debug(err);
this._stopLocking();
});
}, 1000);
}
_stopLocking() {
if (this.updater)
clearInterval(this.updater);
this._count = 0;
}
_debugReport(action, reason) {
this.debug(`${action} ${this.count} ${reason ? `${reason} ` : ''}${this.dirPath}`);
}
}
tslib_1.__decorate([
decorators_1.onceAtATime()
], Lockfile.prototype, "unlock", null);
tslib_1.__decorate([
decorators_1.onceAtATime()
], Lockfile.prototype, "check", null);
tslib_1.__decorate([
decorators_1.onceAtATime()
], Lockfile.prototype, "_add", null);
exports.default = Lockfile;
const instances = [];
process.once('exit', () => {
for (let i of instances) {
try {
i.unlockSync();
}
catch (err) { }
}
});
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function random(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}