UNPKG

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