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