lockfile.js 8.0 KB

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