UNPKG

9.54 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const path = require("path");
5const errors_1 = require("./errors");
6const decorators_1 = require("./decorators");
7const version = require('../package.json').version;
8class Lockfile {
9 /**
10 * creates a new simple lockfile without read/write support
11 */
12 constructor(base, options = {}) {
13 this.timeout = 30000;
14 this.retryInterval = 10;
15 this.stale = 10000;
16 this._count = 0;
17 this.timeout = options.timeout || this.timeout;
18 this.retryInterval = options.retryInterval || this.retryInterval;
19 this.base = base;
20 this._debug = options.debug;
21 this.fs = require('fs-extra');
22 this.uuid = require('uuid/v4')();
23 instances.push(this);
24 }
25 get count() {
26 return this._count;
27 }
28 get dirPath() {
29 return path.resolve(this.base + '.lock');
30 }
31 /**
32 * creates a lock
33 * same as add
34 */
35 lock() {
36 return this.add();
37 }
38 /**
39 * creates a lock
40 * same as add
41 */
42 lockSync() {
43 this.addSync();
44 }
45 /**
46 * removes all lock counts
47 */
48 async unlock() {
49 if (!this.count)
50 return;
51 this._debugReport('unlock');
52 await this.fs.remove(this.dirPath);
53 await this.fs.remove(this._infoPath);
54 this._stopLocking();
55 }
56 /**
57 * removes all lock counts
58 */
59 unlockSync() {
60 if (!this.count)
61 return;
62 this._debugReport('unlock');
63 this.fs.removeSync(this.dirPath);
64 this.fs.removeSync(this._infoPath);
65 this._stopLocking();
66 }
67 /**
68 * adds 1 lock count
69 */
70 async add(opts = {}) {
71 this._debugReport('add', opts.reason);
72 if (!this.count)
73 await this._add(opts);
74 this._count++;
75 }
76 /**
77 * adds 1 lock count
78 */
79 addSync(opts = {}) {
80 this._debugReport('add', opts.reason);
81 if (!this.count)
82 this._lockSync(opts);
83 this._count++;
84 }
85 /**
86 * removes 1 lock count
87 */
88 async remove() {
89 this._debugReport('remove');
90 switch (this.count) {
91 case 0:
92 break;
93 case 1:
94 await this.unlock();
95 break;
96 default:
97 this._count--;
98 break;
99 }
100 }
101 /**
102 * removes 1 lock count
103 */
104 removeSync() {
105 switch (this.count) {
106 case 0:
107 break;
108 case 1:
109 this.unlockSync();
110 break;
111 default:
112 this._count--;
113 break;
114 }
115 }
116 /**
117 * check if this instance can get a lock
118 * returns true if it already has a lock
119 */
120 async check() {
121 const mtime = await this.fetchMtime();
122 const status = this._status(mtime);
123 return ['open', 'have_lock', 'stale'].includes(status);
124 }
125 /**
126 * check if this instance can get a lock
127 * returns true if it already has a lock
128 */
129 checkSync() {
130 const mtime = this.fetchMtimeSync();
131 const status = this._status(mtime);
132 return ['open', 'have_lock', 'stale'].includes(status);
133 }
134 get _infoPath() {
135 return path.resolve(this.dirPath + '.info.json');
136 }
137 async fetchReason() {
138 try {
139 const b = await this.fs.readJSON(this._infoPath);
140 return b.reason;
141 }
142 catch (err) {
143 if (err.code !== 'ENOENT')
144 this.debug(err);
145 }
146 }
147 fetchReasonSync() {
148 try {
149 const b = this.fs.readJSONSync(this._infoPath);
150 return b.reason;
151 }
152 catch (err) {
153 if (err.code !== 'ENOENT')
154 this.debug(err);
155 }
156 }
157 async _saveReason(reason) {
158 try {
159 await this.fs.writeJSON(this._infoPath, {
160 version,
161 uuid: this.uuid,
162 pid: process.pid,
163 reason,
164 });
165 }
166 catch (err) {
167 this.debug(err);
168 }
169 }
170 _saveReasonSync(reason) {
171 try {
172 this.fs.writeJSONSync(this._infoPath, {
173 version,
174 uuid: this.uuid,
175 pid: process.pid,
176 reason,
177 });
178 }
179 catch (err) {
180 this.debug(err);
181 }
182 }
183 async fetchMtime() {
184 try {
185 const { mtime } = await this.fs.stat(this.dirPath);
186 return mtime;
187 }
188 catch (err) {
189 if (err.code !== 'ENOENT')
190 throw err;
191 }
192 }
193 fetchMtimeSync() {
194 try {
195 const { mtime } = this.fs.statSync(this.dirPath);
196 return mtime;
197 }
198 catch (err) {
199 if (err.code !== 'ENOENT')
200 throw err;
201 }
202 }
203 isStale(mtime) {
204 if (!mtime)
205 return true;
206 return mtime < new Date(Date.now() - this.stale);
207 }
208 debug(msg, ...args) {
209 if (this._debug)
210 this._debug(msg, ...args);
211 }
212 async _add(opts) {
213 await this._lock(Object.assign({ timeout: this.timeout, retryInterval: this.retryInterval, ifLocked: ({ reason: _ }) => { } }, opts));
214 await this._saveReason(opts.reason);
215 this.startLocking();
216 }
217 async _lock(opts) {
218 this.debug('_lock', this.dirPath);
219 await this.fs.mkdirp(path.dirname(this.dirPath));
220 try {
221 await this.fs.mkdir(this.dirPath);
222 }
223 catch (err) {
224 if (!['EEXIST', 'EPERM'].includes(err.code))
225 throw err;
226 // grab reason
227 const reason = await this.fetchReason();
228 this.debug('waiting for lock', reason, this.dirPath);
229 // run callback
230 await opts.ifLocked({ reason });
231 // check if timed out
232 if (opts.timeout < 0)
233 throw new errors_1.LockfileError({ reason, file: this.dirPath });
234 // check if stale
235 const mtime = await this.fetchMtime();
236 const status = this._status(mtime);
237 switch (status) {
238 case 'stale':
239 try {
240 await this.fs.rmdir(this.dirPath);
241 }
242 catch (err) {
243 if (err.code !== 'ENOENT')
244 throw err;
245 }
246 case 'open':
247 case 'have_lock':
248 return this._lock(opts);
249 }
250 // wait before retrying
251 const interval = random(opts.retryInterval / 2, opts.retryInterval * 2);
252 await wait(interval);
253 return this._lock(Object.assign({}, opts, { timeout: opts.timeout - interval, retryInterval: opts.retryInterval * 2 }));
254 }
255 }
256 _lockSync({ reason, retries = 20 } = {}) {
257 this.debug('_lockSync', this.dirPath);
258 this.fs.mkdirpSync(path.dirname(this.dirPath));
259 try {
260 this.fs.mkdirSync(this.dirPath);
261 }
262 catch (err) {
263 if (!['EEXIST', 'EPERM'].includes(err.code))
264 throw err;
265 // check if stale
266 const mtime = this.fetchMtimeSync();
267 const status = this._status(mtime);
268 if (retries <= 0) {
269 let reason = this.fetchReasonSync();
270 throw new errors_1.LockfileError({ reason, file: this.dirPath });
271 }
272 if (status === 'stale') {
273 try {
274 this.fs.rmdirSync(this.dirPath);
275 }
276 catch (err) {
277 if (!['EPERM', 'ENOENT'].includes(err.code))
278 throw err;
279 }
280 }
281 return this._lockSync({ reason, retries: retries - 1 });
282 }
283 this._saveReasonSync(reason);
284 this.startLocking();
285 }
286 _status(mtime) {
287 if (this.count)
288 return 'have_lock';
289 if (!mtime)
290 return 'open';
291 const stale = this.isStale(mtime);
292 if (mtime && stale)
293 return 'stale';
294 return 'locked';
295 }
296 startLocking() {
297 this.updater = setInterval(() => {
298 let now = Date.now() / 1000;
299 this.fs.utimes(this.dirPath, now, now).catch(err => {
300 this.debug(err);
301 this._stopLocking();
302 });
303 }, 1000);
304 }
305 _stopLocking() {
306 if (this.updater)
307 clearInterval(this.updater);
308 this._count = 0;
309 }
310 _debugReport(action, reason) {
311 this.debug(`${action} ${this.count} ${reason ? `${reason} ` : ''}${this.dirPath}`);
312 }
313}
314tslib_1.__decorate([
315 decorators_1.onceAtATime()
316], Lockfile.prototype, "unlock", null);
317tslib_1.__decorate([
318 decorators_1.onceAtATime()
319], Lockfile.prototype, "check", null);
320tslib_1.__decorate([
321 decorators_1.onceAtATime()
322], Lockfile.prototype, "_add", null);
323exports.default = Lockfile;
324const instances = [];
325process.once('exit', () => {
326 for (let i of instances) {
327 try {
328 i.unlockSync();
329 }
330 catch (err) { }
331 }
332});
333function wait(ms) {
334 return new Promise(resolve => setTimeout(resolve, ms));
335}
336function random(min, max) {
337 return Math.floor(Math.random() * (max - min) + min);
338}