lockfile.js 8.2 KB

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