• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1var fs = require('fs')
2
3var wx = 'wx'
4if (process.version.match(/^v0\.[0-6]/)) {
5  var c = require('constants')
6  wx = c.O_TRUNC | c.O_CREAT | c.O_WRONLY | c.O_EXCL
7}
8
9var os = require('os')
10exports.filetime = 'ctime'
11if (os.platform() == "win32") {
12  exports.filetime = 'mtime'
13}
14
15var debug
16var util = require('util')
17if (util.debuglog)
18  debug = util.debuglog('LOCKFILE')
19else if (/\blockfile\b/i.test(process.env.NODE_DEBUG))
20  debug = function() {
21    var msg = util.format.apply(util, arguments)
22    console.error('LOCKFILE %d %s', process.pid, msg)
23  }
24else
25  debug = function() {}
26
27var locks = {}
28
29function hasOwnProperty (obj, prop) {
30  return Object.prototype.hasOwnProperty.call(obj, prop)
31}
32
33var onExit = require('signal-exit')
34onExit(function () {
35  debug('exit listener')
36  // cleanup
37  Object.keys(locks).forEach(exports.unlockSync)
38})
39
40// XXX https://github.com/joyent/node/issues/3555
41// Remove when node 0.8 is deprecated.
42if (/^v0\.[0-8]\./.test(process.version)) {
43  debug('uncaughtException, version = %s', process.version)
44  process.on('uncaughtException', function H (er) {
45    debug('uncaughtException')
46    var l = process.listeners('uncaughtException').filter(function (h) {
47      return h !== H
48    })
49    if (!l.length) {
50      // cleanup
51      try { Object.keys(locks).forEach(exports.unlockSync) } catch (e) {}
52      process.removeListener('uncaughtException', H)
53      throw er
54    }
55  })
56}
57
58exports.unlock = function (path, cb) {
59  debug('unlock', path)
60  // best-effort.  unlocking an already-unlocked lock is a noop
61  delete locks[path]
62  fs.unlink(path, function (unlinkEr) { cb && cb() })
63}
64
65exports.unlockSync = function (path) {
66  debug('unlockSync', path)
67  // best-effort.  unlocking an already-unlocked lock is a noop
68  try { fs.unlinkSync(path) } catch (er) {}
69  delete locks[path]
70}
71
72
73// if the file can be opened in readonly mode, then it's there.
74// if the error is something other than ENOENT, then it's not.
75exports.check = function (path, opts, cb) {
76  if (typeof opts === 'function') cb = opts, opts = {}
77  debug('check', path, opts)
78  fs.open(path, 'r', function (er, fd) {
79    if (er) {
80      if (er.code !== 'ENOENT') return cb(er)
81      return cb(null, false)
82    }
83
84    if (!opts.stale) {
85      return fs.close(fd, function (er) {
86        return cb(er, true)
87      })
88    }
89
90    fs.fstat(fd, function (er, st) {
91      if (er) return fs.close(fd, function (er2) {
92        return cb(er)
93      })
94
95      fs.close(fd, function (er) {
96        var age = Date.now() - st[exports.filetime].getTime()
97        return cb(er, age <= opts.stale)
98      })
99    })
100  })
101}
102
103exports.checkSync = function (path, opts) {
104  opts = opts || {}
105  debug('checkSync', path, opts)
106  if (opts.wait) {
107    throw new Error('opts.wait not supported sync for obvious reasons')
108  }
109
110  try {
111    var fd = fs.openSync(path, 'r')
112  } catch (er) {
113    if (er.code !== 'ENOENT') throw er
114    return false
115  }
116
117  if (!opts.stale) {
118    try { fs.closeSync(fd) } catch (er) {}
119    return true
120  }
121
122  // file exists.  however, might be stale
123  if (opts.stale) {
124    try {
125      var st = fs.fstatSync(fd)
126    } finally {
127      fs.closeSync(fd)
128    }
129    var age = Date.now() - st[exports.filetime].getTime()
130    return (age <= opts.stale)
131  }
132}
133
134
135
136var req = 1
137exports.lock = function (path, opts, cb) {
138  if (typeof opts === 'function') cb = opts, opts = {}
139  opts.req = opts.req || req++
140  debug('lock', path, opts)
141  opts.start = opts.start || Date.now()
142
143  if (typeof opts.retries === 'number' && opts.retries > 0) {
144    debug('has retries', opts.retries)
145    var retries = opts.retries
146    opts.retries = 0
147    cb = (function (orig) { return function cb (er, fd) {
148      debug('retry-mutated callback')
149      retries -= 1
150      if (!er || retries < 0) return orig(er, fd)
151
152      debug('lock retry', path, opts)
153
154      if (opts.retryWait) setTimeout(retry, opts.retryWait)
155      else retry()
156
157      function retry () {
158        opts.start = Date.now()
159        debug('retrying', opts.start)
160        exports.lock(path, opts, cb)
161      }
162    }})(cb)
163  }
164
165  // try to engage the lock.
166  // if this succeeds, then we're in business.
167  fs.open(path, wx, function (er, fd) {
168    if (!er) {
169      debug('locked', path, fd)
170      locks[path] = fd
171      return fs.close(fd, function () {
172        return cb()
173      })
174    }
175
176    debug('failed to acquire lock', er)
177
178    // something other than "currently locked"
179    // maybe eperm or something.
180    if (er.code !== 'EEXIST') {
181      debug('not EEXIST error', er)
182      return cb(er)
183    }
184
185    // someone's got this one.  see if it's valid.
186    if (!opts.stale) return notStale(er, path, opts, cb)
187
188    return maybeStale(er, path, opts, false, cb)
189  })
190  debug('lock return')
191}
192
193
194// Staleness checking algorithm
195// 1. acquire $lock, fail
196// 2. stat $lock, find that it is stale
197// 3. acquire $lock.STALE
198// 4. stat $lock, assert that it is still stale
199// 5. unlink $lock
200// 6. link $lock.STALE $lock
201// 7. unlink $lock.STALE
202// On any failure, clean up whatever we've done, and raise the error.
203function maybeStale (originalEr, path, opts, hasStaleLock, cb) {
204  fs.stat(path, function (statEr, st) {
205    if (statEr) {
206      if (statEr.code === 'ENOENT') {
207        // expired already!
208        opts.stale = false
209        debug('lock stale enoent retry', path, opts)
210        exports.lock(path, opts, cb)
211        return
212      }
213      return cb(statEr)
214    }
215
216    var age = Date.now() - st[exports.filetime].getTime()
217    if (age <= opts.stale) return notStale(originalEr, path, opts, cb)
218
219    debug('lock stale', path, opts)
220    if (hasStaleLock) {
221      exports.unlock(path, function (er) {
222        if (er) return cb(er)
223        debug('lock stale retry', path, opts)
224        fs.link(path + '.STALE', path, function (er) {
225          fs.unlink(path + '.STALE', function () {
226            // best effort.  if the unlink fails, oh well.
227            cb(er)
228          })
229        })
230      })
231    } else {
232      debug('acquire .STALE file lock', opts)
233      exports.lock(path + '.STALE', opts, function (er) {
234        if (er) return cb(er)
235        maybeStale(originalEr, path, opts, true, cb)
236      })
237    }
238  })
239}
240
241function notStale (er, path, opts, cb) {
242  debug('notStale', path, opts)
243
244  // if we can't wait, then just call it a failure
245  if (typeof opts.wait !== 'number' || opts.wait <= 0) {
246    debug('notStale, wait is not a number')
247    return cb(er)
248  }
249
250  // poll for some ms for the lock to clear
251  var now = Date.now()
252  var start = opts.start || now
253  var end = start + opts.wait
254
255  if (end <= now)
256    return cb(er)
257
258  debug('now=%d, wait until %d (delta=%d)', start, end, end-start)
259  var wait = Math.min(end - start, opts.pollPeriod || 100)
260  var timer = setTimeout(poll, wait)
261
262  function poll () {
263    debug('notStale, polling', path, opts)
264    exports.lock(path, opts, cb)
265  }
266}
267
268exports.lockSync = function (path, opts) {
269  opts = opts || {}
270  opts.req = opts.req || req++
271  debug('lockSync', path, opts)
272  if (opts.wait || opts.retryWait) {
273    throw new Error('opts.wait not supported sync for obvious reasons')
274  }
275
276  try {
277    var fd = fs.openSync(path, wx)
278    locks[path] = fd
279    try { fs.closeSync(fd) } catch (er) {}
280    debug('locked sync!', path, fd)
281    return
282  } catch (er) {
283    if (er.code !== 'EEXIST') return retryThrow(path, opts, er)
284
285    if (opts.stale) {
286      var st = fs.statSync(path)
287      var ct = st[exports.filetime].getTime()
288      if (!(ct % 1000) && (opts.stale % 1000)) {
289        // probably don't have subsecond resolution.
290        // round up the staleness indicator.
291        // Yes, this will be wrong 1/1000 times on platforms
292        // with subsecond stat precision, but that's acceptable
293        // in exchange for not mistakenly removing locks on
294        // most other systems.
295        opts.stale = 1000 * Math.ceil(opts.stale / 1000)
296      }
297      var age = Date.now() - ct
298      if (age > opts.stale) {
299        debug('lockSync stale', path, opts, age)
300        exports.unlockSync(path)
301        return exports.lockSync(path, opts)
302      }
303    }
304
305    // failed to lock!
306    debug('failed to lock', path, opts, er)
307    return retryThrow(path, opts, er)
308  }
309}
310
311function retryThrow (path, opts, er) {
312  if (typeof opts.retries === 'number' && opts.retries > 0) {
313    var newRT = opts.retries - 1
314    debug('retryThrow', path, opts, newRT)
315    opts.retries = newRT
316    return exports.lockSync(path, opts)
317  }
318  throw er
319}
320
321