UNPKG

rwlockfile

Version:

lockfile utility with reader/writers

339 lines (338 loc) 9.54 kB
"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); }