##// END OF EJS Templates
inotify: avoid passing potentially stale stat to update
Matt Mackall -
r7220:60826e07 default
parent child Browse files
Show More
@@ -1,737 +1,737 b''
1 # server.py - inotify status server
1 # server.py - inotify status server
2 #
2 #
3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 from mercurial.i18n import _
9 from mercurial.i18n import _
10 from mercurial import osutil, ui, util
10 from mercurial import osutil, ui, util
11 import common
11 import common
12 import errno, os, select, socket, stat, struct, sys, tempfile, time
12 import errno, os, select, socket, stat, struct, sys, tempfile, time
13
13
14 try:
14 try:
15 import linux as inotify
15 import linux as inotify
16 from linux import watcher
16 from linux import watcher
17 except ImportError:
17 except ImportError:
18 print >> sys.stderr, '*** native support is required for this extension'
18 print >> sys.stderr, '*** native support is required for this extension'
19 raise
19 raise
20
20
21 class AlreadyStartedException(Exception): pass
21 class AlreadyStartedException(Exception): pass
22
22
23 def join(a, b):
23 def join(a, b):
24 if a:
24 if a:
25 if a[-1] == '/':
25 if a[-1] == '/':
26 return a + b
26 return a + b
27 return a + '/' + b
27 return a + '/' + b
28 return b
28 return b
29
29
30 walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
30 walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
31
31
32 def walkrepodirs(repo):
32 def walkrepodirs(repo):
33 '''Iterate over all subdirectories of this repo.
33 '''Iterate over all subdirectories of this repo.
34 Exclude the .hg directory, any nested repos, and ignored dirs.'''
34 Exclude the .hg directory, any nested repos, and ignored dirs.'''
35 rootslash = repo.root + os.sep
35 rootslash = repo.root + os.sep
36 def walkit(dirname, top):
36 def walkit(dirname, top):
37 hginside = False
37 hginside = False
38 try:
38 try:
39 for name, kind in osutil.listdir(rootslash + dirname):
39 for name, kind in osutil.listdir(rootslash + dirname):
40 if kind == stat.S_IFDIR:
40 if kind == stat.S_IFDIR:
41 if name == '.hg':
41 if name == '.hg':
42 hginside = True
42 hginside = True
43 if not top: break
43 if not top: break
44 else:
44 else:
45 d = join(dirname, name)
45 d = join(dirname, name)
46 if repo.dirstate._ignore(d):
46 if repo.dirstate._ignore(d):
47 continue
47 continue
48 for subdir, hginsub in walkit(d, False):
48 for subdir, hginsub in walkit(d, False):
49 if not hginsub:
49 if not hginsub:
50 yield subdir, False
50 yield subdir, False
51 except OSError, err:
51 except OSError, err:
52 if err.errno not in walk_ignored_errors:
52 if err.errno not in walk_ignored_errors:
53 raise
53 raise
54 yield rootslash + dirname, hginside
54 yield rootslash + dirname, hginside
55 for dirname, hginside in walkit('', True):
55 for dirname, hginside in walkit('', True):
56 yield dirname
56 yield dirname
57
57
58 def walk(repo, root):
58 def walk(repo, root):
59 '''Like os.walk, but only yields regular files.'''
59 '''Like os.walk, but only yields regular files.'''
60
60
61 # This function is critical to performance during startup.
61 # This function is critical to performance during startup.
62
62
63 reporoot = root == ''
63 reporoot = root == ''
64 rootslash = repo.root + os.sep
64 rootslash = repo.root + os.sep
65
65
66 def walkit(root, reporoot):
66 def walkit(root, reporoot):
67 files, dirs = [], []
67 files, dirs = [], []
68 hginside = False
68 hginside = False
69
69
70 try:
70 try:
71 fullpath = rootslash + root
71 fullpath = rootslash + root
72 for name, kind in osutil.listdir(fullpath):
72 for name, kind in osutil.listdir(fullpath):
73 if kind == stat.S_IFDIR:
73 if kind == stat.S_IFDIR:
74 if name == '.hg':
74 if name == '.hg':
75 hginside = True
75 hginside = True
76 if reporoot:
76 if reporoot:
77 continue
77 continue
78 else:
78 else:
79 break
79 break
80 dirs.append(name)
80 dirs.append(name)
81 elif kind in (stat.S_IFREG, stat.S_IFLNK):
81 elif kind in (stat.S_IFREG, stat.S_IFLNK):
82 path = join(root, name)
82 path = join(root, name)
83 files.append((name, kind))
83 files.append((name, kind))
84
84
85 yield hginside, fullpath, dirs, files
85 yield hginside, fullpath, dirs, files
86
86
87 for subdir in dirs:
87 for subdir in dirs:
88 path = join(root, subdir)
88 path = join(root, subdir)
89 if repo.dirstate._ignore(path):
89 if repo.dirstate._ignore(path):
90 continue
90 continue
91 for result in walkit(path, False):
91 for result in walkit(path, False):
92 if not result[0]:
92 if not result[0]:
93 yield result
93 yield result
94 except OSError, err:
94 except OSError, err:
95 if err.errno not in walk_ignored_errors:
95 if err.errno not in walk_ignored_errors:
96 raise
96 raise
97 for result in walkit(root, reporoot):
97 for result in walkit(root, reporoot):
98 yield result[1:]
98 yield result[1:]
99
99
100 def _explain_watch_limit(ui, repo, count):
100 def _explain_watch_limit(ui, repo, count):
101 path = '/proc/sys/fs/inotify/max_user_watches'
101 path = '/proc/sys/fs/inotify/max_user_watches'
102 try:
102 try:
103 limit = int(file(path).read())
103 limit = int(file(path).read())
104 except IOError, err:
104 except IOError, err:
105 if err.errno != errno.ENOENT:
105 if err.errno != errno.ENOENT:
106 raise
106 raise
107 raise util.Abort(_('this system does not seem to '
107 raise util.Abort(_('this system does not seem to '
108 'support inotify'))
108 'support inotify'))
109 ui.warn(_('*** the current per-user limit on the number '
109 ui.warn(_('*** the current per-user limit on the number '
110 'of inotify watches is %s\n') % limit)
110 'of inotify watches is %s\n') % limit)
111 ui.warn(_('*** this limit is too low to watch every '
111 ui.warn(_('*** this limit is too low to watch every '
112 'directory in this repository\n'))
112 'directory in this repository\n'))
113 ui.warn(_('*** counting directories: '))
113 ui.warn(_('*** counting directories: '))
114 ndirs = len(list(walkrepodirs(repo)))
114 ndirs = len(list(walkrepodirs(repo)))
115 ui.warn(_('found %d\n') % ndirs)
115 ui.warn(_('found %d\n') % ndirs)
116 newlimit = min(limit, 1024)
116 newlimit = min(limit, 1024)
117 while newlimit < ((limit + ndirs) * 1.1):
117 while newlimit < ((limit + ndirs) * 1.1):
118 newlimit *= 2
118 newlimit *= 2
119 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
119 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
120 (limit, newlimit))
120 (limit, newlimit))
121 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
121 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
122 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
122 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
123 % repo.root)
123 % repo.root)
124
124
125 class Watcher(object):
125 class Watcher(object):
126 poll_events = select.POLLIN
126 poll_events = select.POLLIN
127 statuskeys = 'almr!?'
127 statuskeys = 'almr!?'
128
128
129 def __init__(self, ui, repo, master):
129 def __init__(self, ui, repo, master):
130 self.ui = ui
130 self.ui = ui
131 self.repo = repo
131 self.repo = repo
132 self.wprefix = self.repo.wjoin('')
132 self.wprefix = self.repo.wjoin('')
133 self.timeout = None
133 self.timeout = None
134 self.master = master
134 self.master = master
135 self.mask = (
135 self.mask = (
136 inotify.IN_ATTRIB |
136 inotify.IN_ATTRIB |
137 inotify.IN_CREATE |
137 inotify.IN_CREATE |
138 inotify.IN_DELETE |
138 inotify.IN_DELETE |
139 inotify.IN_DELETE_SELF |
139 inotify.IN_DELETE_SELF |
140 inotify.IN_MODIFY |
140 inotify.IN_MODIFY |
141 inotify.IN_MOVED_FROM |
141 inotify.IN_MOVED_FROM |
142 inotify.IN_MOVED_TO |
142 inotify.IN_MOVED_TO |
143 inotify.IN_MOVE_SELF |
143 inotify.IN_MOVE_SELF |
144 inotify.IN_ONLYDIR |
144 inotify.IN_ONLYDIR |
145 inotify.IN_UNMOUNT |
145 inotify.IN_UNMOUNT |
146 0)
146 0)
147 try:
147 try:
148 self.watcher = watcher.Watcher()
148 self.watcher = watcher.Watcher()
149 except OSError, err:
149 except OSError, err:
150 raise util.Abort(_('inotify service not available: %s') %
150 raise util.Abort(_('inotify service not available: %s') %
151 err.strerror)
151 err.strerror)
152 self.threshold = watcher.Threshold(self.watcher)
152 self.threshold = watcher.Threshold(self.watcher)
153 self.registered = True
153 self.registered = True
154 self.fileno = self.watcher.fileno
154 self.fileno = self.watcher.fileno
155
155
156 self.repo.dirstate.__class__.inotifyserver = True
156 self.repo.dirstate.__class__.inotifyserver = True
157
157
158 self.tree = {}
158 self.tree = {}
159 self.statcache = {}
159 self.statcache = {}
160 self.statustrees = dict([(s, {}) for s in self.statuskeys])
160 self.statustrees = dict([(s, {}) for s in self.statuskeys])
161
161
162 self.watches = 0
162 self.watches = 0
163 self.last_event = None
163 self.last_event = None
164
164
165 self.eventq = {}
165 self.eventq = {}
166 self.deferred = 0
166 self.deferred = 0
167
167
168 self.ds_info = self.dirstate_info()
168 self.ds_info = self.dirstate_info()
169 self.scan()
169 self.scan()
170
170
171 def event_time(self):
171 def event_time(self):
172 last = self.last_event
172 last = self.last_event
173 now = time.time()
173 now = time.time()
174 self.last_event = now
174 self.last_event = now
175
175
176 if last is None:
176 if last is None:
177 return 'start'
177 return 'start'
178 delta = now - last
178 delta = now - last
179 if delta < 5:
179 if delta < 5:
180 return '+%.3f' % delta
180 return '+%.3f' % delta
181 if delta < 50:
181 if delta < 50:
182 return '+%.2f' % delta
182 return '+%.2f' % delta
183 return '+%.1f' % delta
183 return '+%.1f' % delta
184
184
185 def dirstate_info(self):
185 def dirstate_info(self):
186 try:
186 try:
187 st = os.lstat(self.repo.join('dirstate'))
187 st = os.lstat(self.repo.join('dirstate'))
188 return st.st_mtime, st.st_ino
188 return st.st_mtime, st.st_ino
189 except OSError, err:
189 except OSError, err:
190 if err.errno != errno.ENOENT:
190 if err.errno != errno.ENOENT:
191 raise
191 raise
192 return 0, 0
192 return 0, 0
193
193
194 def add_watch(self, path, mask):
194 def add_watch(self, path, mask):
195 if not path:
195 if not path:
196 return
196 return
197 if self.watcher.path(path) is None:
197 if self.watcher.path(path) is None:
198 if self.ui.debugflag:
198 if self.ui.debugflag:
199 self.ui.note(_('watching %r\n') % path[len(self.wprefix):])
199 self.ui.note(_('watching %r\n') % path[len(self.wprefix):])
200 try:
200 try:
201 self.watcher.add(path, mask)
201 self.watcher.add(path, mask)
202 self.watches += 1
202 self.watches += 1
203 except OSError, err:
203 except OSError, err:
204 if err.errno in (errno.ENOENT, errno.ENOTDIR):
204 if err.errno in (errno.ENOENT, errno.ENOTDIR):
205 return
205 return
206 if err.errno != errno.ENOSPC:
206 if err.errno != errno.ENOSPC:
207 raise
207 raise
208 _explain_watch_limit(self.ui, self.repo, self.watches)
208 _explain_watch_limit(self.ui, self.repo, self.watches)
209
209
210 def setup(self):
210 def setup(self):
211 self.ui.note(_('watching directories under %r\n') % self.repo.root)
211 self.ui.note(_('watching directories under %r\n') % self.repo.root)
212 self.add_watch(self.repo.path, inotify.IN_DELETE)
212 self.add_watch(self.repo.path, inotify.IN_DELETE)
213 self.check_dirstate()
213 self.check_dirstate()
214
214
215 def wpath(self, evt):
215 def wpath(self, evt):
216 path = evt.fullpath
216 path = evt.fullpath
217 if path == self.repo.root:
217 if path == self.repo.root:
218 return ''
218 return ''
219 if path.startswith(self.wprefix):
219 if path.startswith(self.wprefix):
220 return path[len(self.wprefix):]
220 return path[len(self.wprefix):]
221 raise 'wtf? ' + path
221 raise 'wtf? ' + path
222
222
223 def dir(self, tree, path):
223 def dir(self, tree, path):
224 if path:
224 if path:
225 for name in path.split('/'):
225 for name in path.split('/'):
226 tree.setdefault(name, {})
226 tree.setdefault(name, {})
227 tree = tree[name]
227 tree = tree[name]
228 return tree
228 return tree
229
229
230 def lookup(self, path, tree):
230 def lookup(self, path, tree):
231 if path:
231 if path:
232 try:
232 try:
233 for name in path.split('/'):
233 for name in path.split('/'):
234 tree = tree[name]
234 tree = tree[name]
235 except KeyError:
235 except KeyError:
236 return 'x'
236 return 'x'
237 except TypeError:
237 except TypeError:
238 return 'd'
238 return 'd'
239 return tree
239 return tree
240
240
241 def split(self, path):
241 def split(self, path):
242 c = path.rfind('/')
242 c = path.rfind('/')
243 if c == -1:
243 if c == -1:
244 return '', path
244 return '', path
245 return path[:c], path[c+1:]
245 return path[:c], path[c+1:]
246
246
247 def filestatus(self, fn, st):
247 def filestatus(self, fn, st):
248 try:
248 try:
249 type_, mode, size, time = self.repo.dirstate._map[fn][:4]
249 type_, mode, size, time = self.repo.dirstate._map[fn][:4]
250 except KeyError:
250 except KeyError:
251 type_ = '?'
251 type_ = '?'
252 if type_ == 'n':
252 if type_ == 'n':
253 if not st:
253 if not st:
254 return '!'
254 return '!'
255 st_mode, st_size, st_mtime = st
255 st_mode, st_size, st_mtime = st
256 if size == -1:
256 if size == -1:
257 return 'l'
257 return 'l'
258 if size and (size != st_size or (mode ^ st_mode) & 0100):
258 if size and (size != st_size or (mode ^ st_mode) & 0100):
259 return 'm'
259 return 'm'
260 if time != int(st_mtime):
260 if time != int(st_mtime):
261 return 'l'
261 return 'l'
262 return 'n'
262 return 'n'
263 if type_ in 'ma' and not st:
263 if type_ in 'ma' and not st:
264 return '!'
264 return '!'
265 if type_ == '?' and self.repo.dirstate._ignore(fn):
265 if type_ == '?' and self.repo.dirstate._ignore(fn):
266 return 'i'
266 return 'i'
267 return type_
267 return type_
268
268
269 def updatestatus(self, wfn, st=None, status=None):
269 def updatestatus(self, wfn, st=None, status=None):
270 if st:
270 if st:
271 status = self.filestatus(wfn, st)
271 status = self.filestatus(wfn, st)
272 else:
272 else:
273 self.statcache.pop(wfn, None)
273 self.statcache.pop(wfn, None)
274 root, fn = self.split(wfn)
274 root, fn = self.split(wfn)
275 d = self.dir(self.tree, root)
275 d = self.dir(self.tree, root)
276 oldstatus = d.get(fn)
276 oldstatus = d.get(fn)
277 isdir = False
277 isdir = False
278 if oldstatus:
278 if oldstatus:
279 try:
279 try:
280 if not status:
280 if not status:
281 if oldstatus in 'almn':
281 if oldstatus in 'almn':
282 status = '!'
282 status = '!'
283 elif oldstatus == 'r':
283 elif oldstatus == 'r':
284 status = 'r'
284 status = 'r'
285 except TypeError:
285 except TypeError:
286 # oldstatus may be a dict left behind by a deleted
286 # oldstatus may be a dict left behind by a deleted
287 # directory
287 # directory
288 isdir = True
288 isdir = True
289 else:
289 else:
290 if oldstatus in self.statuskeys and oldstatus != status:
290 if oldstatus in self.statuskeys and oldstatus != status:
291 del self.dir(self.statustrees[oldstatus], root)[fn]
291 del self.dir(self.statustrees[oldstatus], root)[fn]
292 if self.ui.debugflag and oldstatus != status:
292 if self.ui.debugflag and oldstatus != status:
293 if isdir:
293 if isdir:
294 self.ui.note(_('status: %r dir(%d) -> %s\n') %
294 self.ui.note(_('status: %r dir(%d) -> %s\n') %
295 (wfn, len(oldstatus), status))
295 (wfn, len(oldstatus), status))
296 else:
296 else:
297 self.ui.note(_('status: %r %s -> %s\n') %
297 self.ui.note(_('status: %r %s -> %s\n') %
298 (wfn, oldstatus, status))
298 (wfn, oldstatus, status))
299 if not isdir:
299 if not isdir:
300 if status and status != 'i':
300 if status and status != 'i':
301 d[fn] = status
301 d[fn] = status
302 if status in self.statuskeys:
302 if status in self.statuskeys:
303 dd = self.dir(self.statustrees[status], root)
303 dd = self.dir(self.statustrees[status], root)
304 if oldstatus != status or fn not in dd:
304 if oldstatus != status or fn not in dd:
305 dd[fn] = status
305 dd[fn] = status
306 else:
306 else:
307 d.pop(fn, None)
307 d.pop(fn, None)
308
308
309 def check_deleted(self, key):
309 def check_deleted(self, key):
310 # Files that had been deleted but were present in the dirstate
310 # Files that had been deleted but were present in the dirstate
311 # may have vanished from the dirstate; we must clean them up.
311 # may have vanished from the dirstate; we must clean them up.
312 nuke = []
312 nuke = []
313 for wfn, ignore in self.walk(key, self.statustrees[key]):
313 for wfn, ignore in self.walk(key, self.statustrees[key]):
314 if wfn not in self.repo.dirstate:
314 if wfn not in self.repo.dirstate:
315 nuke.append(wfn)
315 nuke.append(wfn)
316 for wfn in nuke:
316 for wfn in nuke:
317 root, fn = self.split(wfn)
317 root, fn = self.split(wfn)
318 del self.dir(self.statustrees[key], root)[fn]
318 del self.dir(self.statustrees[key], root)[fn]
319 del self.dir(self.tree, root)[fn]
319 del self.dir(self.tree, root)[fn]
320
320
321 def scan(self, topdir=''):
321 def scan(self, topdir=''):
322 self.handle_timeout()
322 self.handle_timeout()
323 ds = self.repo.dirstate._map.copy()
323 ds = self.repo.dirstate._map.copy()
324 self.add_watch(join(self.repo.root, topdir), self.mask)
324 self.add_watch(join(self.repo.root, topdir), self.mask)
325 for root, dirs, entries in walk(self.repo, topdir):
325 for root, dirs, entries in walk(self.repo, topdir):
326 for d in dirs:
326 for d in dirs:
327 self.add_watch(join(root, d), self.mask)
327 self.add_watch(join(root, d), self.mask)
328 wroot = root[len(self.wprefix):]
328 wroot = root[len(self.wprefix):]
329 d = self.dir(self.tree, wroot)
329 d = self.dir(self.tree, wroot)
330 for fn, kind in entries:
330 for fn, kind in entries:
331 wfn = join(wroot, fn)
331 wfn = join(wroot, fn)
332 self.updatestatus(wfn, self.getstat(wfn))
332 self.updatestatus(wfn, self.getstat(wfn))
333 ds.pop(wfn, None)
333 ds.pop(wfn, None)
334 wtopdir = topdir
334 wtopdir = topdir
335 if wtopdir and wtopdir[-1] != '/':
335 if wtopdir and wtopdir[-1] != '/':
336 wtopdir += '/'
336 wtopdir += '/'
337 for wfn, state in ds.iteritems():
337 for wfn, state in ds.iteritems():
338 if not wfn.startswith(wtopdir):
338 if not wfn.startswith(wtopdir):
339 continue
339 continue
340 status = state[0]
340 status = state[0]
341 st = self.getstat(wfn)
341 st = self.stat(wfn)
342 if status == 'r' and not st:
342 if status == 'r' and not st:
343 self.updatestatus(wfn, st, status=status)
343 self.updatestatus(wfn, st, status=status)
344 else:
344 else:
345 self.updatestatus(wfn, st)
345 self.updatestatus(wfn, st)
346 self.check_deleted('!')
346 self.check_deleted('!')
347 self.check_deleted('r')
347 self.check_deleted('r')
348
348
349 def check_dirstate(self):
349 def check_dirstate(self):
350 ds_info = self.dirstate_info()
350 ds_info = self.dirstate_info()
351 if ds_info == self.ds_info:
351 if ds_info == self.ds_info:
352 return
352 return
353 self.ds_info = ds_info
353 self.ds_info = ds_info
354 if not self.ui.debugflag:
354 if not self.ui.debugflag:
355 self.last_event = None
355 self.last_event = None
356 self.ui.note(_('%s dirstate reload\n') % self.event_time())
356 self.ui.note(_('%s dirstate reload\n') % self.event_time())
357 self.repo.dirstate.invalidate()
357 self.repo.dirstate.invalidate()
358 self.scan()
358 self.scan()
359 self.ui.note(_('%s end dirstate reload\n') % self.event_time())
359 self.ui.note(_('%s end dirstate reload\n') % self.event_time())
360
360
361 def walk(self, states, tree, prefix=''):
361 def walk(self, states, tree, prefix=''):
362 # This is the "inner loop" when talking to the client.
362 # This is the "inner loop" when talking to the client.
363
363
364 for name, val in tree.iteritems():
364 for name, val in tree.iteritems():
365 path = join(prefix, name)
365 path = join(prefix, name)
366 try:
366 try:
367 if val in states:
367 if val in states:
368 yield path, val
368 yield path, val
369 except TypeError:
369 except TypeError:
370 for p in self.walk(states, val, path):
370 for p in self.walk(states, val, path):
371 yield p
371 yield p
372
372
373 def update_hgignore(self):
373 def update_hgignore(self):
374 # An update of the ignore file can potentially change the
374 # An update of the ignore file can potentially change the
375 # states of all unknown and ignored files.
375 # states of all unknown and ignored files.
376
376
377 # XXX If the user has other ignore files outside the repo, or
377 # XXX If the user has other ignore files outside the repo, or
378 # changes their list of ignore files at run time, we'll
378 # changes their list of ignore files at run time, we'll
379 # potentially never see changes to them. We could get the
379 # potentially never see changes to them. We could get the
380 # client to report to us what ignore data they're using.
380 # client to report to us what ignore data they're using.
381 # But it's easier to do nothing than to open that can of
381 # But it's easier to do nothing than to open that can of
382 # worms.
382 # worms.
383
383
384 if '_ignore' in self.repo.dirstate.__dict__:
384 if '_ignore' in self.repo.dirstate.__dict__:
385 delattr(self.repo.dirstate, '_ignore')
385 delattr(self.repo.dirstate, '_ignore')
386 self.ui.note(_('rescanning due to .hgignore change\n'))
386 self.ui.note(_('rescanning due to .hgignore change\n'))
387 self.scan()
387 self.scan()
388
388
389 def getstat(self, wpath):
389 def getstat(self, wpath):
390 try:
390 try:
391 return self.statcache[wpath]
391 return self.statcache[wpath]
392 except KeyError:
392 except KeyError:
393 try:
393 try:
394 return self.stat(wpath)
394 return self.stat(wpath)
395 except OSError, err:
395 except OSError, err:
396 if err.errno != errno.ENOENT:
396 if err.errno != errno.ENOENT:
397 raise
397 raise
398
398
399 def stat(self, wpath):
399 def stat(self, wpath):
400 try:
400 try:
401 st = os.lstat(join(self.wprefix, wpath))
401 st = os.lstat(join(self.wprefix, wpath))
402 ret = st.st_mode, st.st_size, st.st_mtime
402 ret = st.st_mode, st.st_size, st.st_mtime
403 self.statcache[wpath] = ret
403 self.statcache[wpath] = ret
404 return ret
404 return ret
405 except OSError, err:
405 except OSError, err:
406 self.statcache.pop(wpath, None)
406 self.statcache.pop(wpath, None)
407 raise
407 raise
408
408
409 def created(self, wpath):
409 def created(self, wpath):
410 if wpath == '.hgignore':
410 if wpath == '.hgignore':
411 self.update_hgignore()
411 self.update_hgignore()
412 try:
412 try:
413 st = self.stat(wpath)
413 st = self.stat(wpath)
414 if stat.S_ISREG(st[0]):
414 if stat.S_ISREG(st[0]):
415 self.updatestatus(wpath, st)
415 self.updatestatus(wpath, st)
416 except OSError, err:
416 except OSError, err:
417 pass
417 pass
418
418
419 def modified(self, wpath):
419 def modified(self, wpath):
420 if wpath == '.hgignore':
420 if wpath == '.hgignore':
421 self.update_hgignore()
421 self.update_hgignore()
422 try:
422 try:
423 st = self.stat(wpath)
423 st = self.stat(wpath)
424 if stat.S_ISREG(st[0]):
424 if stat.S_ISREG(st[0]):
425 if self.repo.dirstate[wpath] in 'lmn':
425 if self.repo.dirstate[wpath] in 'lmn':
426 self.updatestatus(wpath, st)
426 self.updatestatus(wpath, st)
427 except OSError:
427 except OSError:
428 pass
428 pass
429
429
430 def deleted(self, wpath):
430 def deleted(self, wpath):
431 if wpath == '.hgignore':
431 if wpath == '.hgignore':
432 self.update_hgignore()
432 self.update_hgignore()
433 elif wpath.startswith('.hg/'):
433 elif wpath.startswith('.hg/'):
434 if wpath == '.hg/wlock':
434 if wpath == '.hg/wlock':
435 self.check_dirstate()
435 self.check_dirstate()
436 return
436 return
437
437
438 self.updatestatus(wpath, None)
438 self.updatestatus(wpath, None)
439
439
440 def schedule_work(self, wpath, evt):
440 def schedule_work(self, wpath, evt):
441 self.eventq.setdefault(wpath, [])
441 self.eventq.setdefault(wpath, [])
442 prev = self.eventq[wpath]
442 prev = self.eventq[wpath]
443 try:
443 try:
444 if prev and evt == 'm' and prev[-1] in 'cm':
444 if prev and evt == 'm' and prev[-1] in 'cm':
445 return
445 return
446 self.eventq[wpath].append(evt)
446 self.eventq[wpath].append(evt)
447 finally:
447 finally:
448 self.deferred += 1
448 self.deferred += 1
449 self.timeout = 250
449 self.timeout = 250
450
450
451 def deferred_event(self, wpath, evt):
451 def deferred_event(self, wpath, evt):
452 if evt == 'c':
452 if evt == 'c':
453 self.created(wpath)
453 self.created(wpath)
454 elif evt == 'm':
454 elif evt == 'm':
455 self.modified(wpath)
455 self.modified(wpath)
456 elif evt == 'd':
456 elif evt == 'd':
457 self.deleted(wpath)
457 self.deleted(wpath)
458
458
459 def process_create(self, wpath, evt):
459 def process_create(self, wpath, evt):
460 if self.ui.debugflag:
460 if self.ui.debugflag:
461 self.ui.note(_('%s event: created %s\n') %
461 self.ui.note(_('%s event: created %s\n') %
462 (self.event_time(), wpath))
462 (self.event_time(), wpath))
463
463
464 if evt.mask & inotify.IN_ISDIR:
464 if evt.mask & inotify.IN_ISDIR:
465 self.scan(wpath)
465 self.scan(wpath)
466 else:
466 else:
467 self.schedule_work(wpath, 'c')
467 self.schedule_work(wpath, 'c')
468
468
469 def process_delete(self, wpath, evt):
469 def process_delete(self, wpath, evt):
470 if self.ui.debugflag:
470 if self.ui.debugflag:
471 self.ui.note(_('%s event: deleted %s\n') %
471 self.ui.note(_('%s event: deleted %s\n') %
472 (self.event_time(), wpath))
472 (self.event_time(), wpath))
473
473
474 if evt.mask & inotify.IN_ISDIR:
474 if evt.mask & inotify.IN_ISDIR:
475 self.scan(wpath)
475 self.scan(wpath)
476 else:
476 else:
477 self.schedule_work(wpath, 'd')
477 self.schedule_work(wpath, 'd')
478
478
479 def process_modify(self, wpath, evt):
479 def process_modify(self, wpath, evt):
480 if self.ui.debugflag:
480 if self.ui.debugflag:
481 self.ui.note(_('%s event: modified %s\n') %
481 self.ui.note(_('%s event: modified %s\n') %
482 (self.event_time(), wpath))
482 (self.event_time(), wpath))
483
483
484 if not (evt.mask & inotify.IN_ISDIR):
484 if not (evt.mask & inotify.IN_ISDIR):
485 self.schedule_work(wpath, 'm')
485 self.schedule_work(wpath, 'm')
486
486
487 def process_unmount(self, evt):
487 def process_unmount(self, evt):
488 self.ui.warn(_('filesystem containing %s was unmounted\n') %
488 self.ui.warn(_('filesystem containing %s was unmounted\n') %
489 evt.fullpath)
489 evt.fullpath)
490 sys.exit(0)
490 sys.exit(0)
491
491
492 def handle_event(self, fd, event):
492 def handle_event(self, fd, event):
493 if self.ui.debugflag:
493 if self.ui.debugflag:
494 self.ui.note(_('%s readable: %d bytes\n') %
494 self.ui.note(_('%s readable: %d bytes\n') %
495 (self.event_time(), self.threshold.readable()))
495 (self.event_time(), self.threshold.readable()))
496 if not self.threshold():
496 if not self.threshold():
497 if self.registered:
497 if self.registered:
498 if self.ui.debugflag:
498 if self.ui.debugflag:
499 self.ui.note(_('%s below threshold - unhooking\n') %
499 self.ui.note(_('%s below threshold - unhooking\n') %
500 (self.event_time()))
500 (self.event_time()))
501 self.master.poll.unregister(fd)
501 self.master.poll.unregister(fd)
502 self.registered = False
502 self.registered = False
503 self.timeout = 250
503 self.timeout = 250
504 else:
504 else:
505 self.read_events()
505 self.read_events()
506
506
507 def read_events(self, bufsize=None):
507 def read_events(self, bufsize=None):
508 events = self.watcher.read(bufsize)
508 events = self.watcher.read(bufsize)
509 if self.ui.debugflag:
509 if self.ui.debugflag:
510 self.ui.note(_('%s reading %d events\n') %
510 self.ui.note(_('%s reading %d events\n') %
511 (self.event_time(), len(events)))
511 (self.event_time(), len(events)))
512 for evt in events:
512 for evt in events:
513 wpath = self.wpath(evt)
513 wpath = self.wpath(evt)
514 if evt.mask & inotify.IN_UNMOUNT:
514 if evt.mask & inotify.IN_UNMOUNT:
515 self.process_unmount(wpath, evt)
515 self.process_unmount(wpath, evt)
516 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
516 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
517 self.process_modify(wpath, evt)
517 self.process_modify(wpath, evt)
518 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
518 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
519 inotify.IN_MOVED_FROM):
519 inotify.IN_MOVED_FROM):
520 self.process_delete(wpath, evt)
520 self.process_delete(wpath, evt)
521 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
521 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
522 self.process_create(wpath, evt)
522 self.process_create(wpath, evt)
523
523
524 def handle_timeout(self):
524 def handle_timeout(self):
525 if not self.registered:
525 if not self.registered:
526 if self.ui.debugflag:
526 if self.ui.debugflag:
527 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
527 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
528 (self.event_time(), self.threshold.readable()))
528 (self.event_time(), self.threshold.readable()))
529 self.read_events(0)
529 self.read_events(0)
530 self.master.poll.register(self, select.POLLIN)
530 self.master.poll.register(self, select.POLLIN)
531 self.registered = True
531 self.registered = True
532
532
533 if self.eventq:
533 if self.eventq:
534 if self.ui.debugflag:
534 if self.ui.debugflag:
535 self.ui.note(_('%s processing %d deferred events as %d\n') %
535 self.ui.note(_('%s processing %d deferred events as %d\n') %
536 (self.event_time(), self.deferred,
536 (self.event_time(), self.deferred,
537 len(self.eventq)))
537 len(self.eventq)))
538 for wpath, evts in util.sort(self.eventq.items()):
538 for wpath, evts in util.sort(self.eventq.items()):
539 for evt in evts:
539 for evt in evts:
540 self.deferred_event(wpath, evt)
540 self.deferred_event(wpath, evt)
541 self.eventq.clear()
541 self.eventq.clear()
542 self.deferred = 0
542 self.deferred = 0
543 self.timeout = None
543 self.timeout = None
544
544
545 def shutdown(self):
545 def shutdown(self):
546 self.watcher.close()
546 self.watcher.close()
547
547
548 class Server(object):
548 class Server(object):
549 poll_events = select.POLLIN
549 poll_events = select.POLLIN
550
550
551 def __init__(self, ui, repo, watcher, timeout):
551 def __init__(self, ui, repo, watcher, timeout):
552 self.ui = ui
552 self.ui = ui
553 self.repo = repo
553 self.repo = repo
554 self.watcher = watcher
554 self.watcher = watcher
555 self.timeout = timeout
555 self.timeout = timeout
556 self.sock = socket.socket(socket.AF_UNIX)
556 self.sock = socket.socket(socket.AF_UNIX)
557 self.sockpath = self.repo.join('inotify.sock')
557 self.sockpath = self.repo.join('inotify.sock')
558 self.realsockpath = None
558 self.realsockpath = None
559 try:
559 try:
560 self.sock.bind(self.sockpath)
560 self.sock.bind(self.sockpath)
561 except socket.error, err:
561 except socket.error, err:
562 if err[0] == errno.EADDRINUSE:
562 if err[0] == errno.EADDRINUSE:
563 raise AlreadyStartedException(_('could not start server: %s')
563 raise AlreadyStartedException(_('could not start server: %s')
564 % err[1])
564 % err[1])
565 if err[0] == "AF_UNIX path too long":
565 if err[0] == "AF_UNIX path too long":
566 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
566 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
567 self.realsockpath = os.path.join(tempdir, "inotify.sock")
567 self.realsockpath = os.path.join(tempdir, "inotify.sock")
568 try:
568 try:
569 self.sock.bind(self.realsockpath)
569 self.sock.bind(self.realsockpath)
570 os.symlink(self.realsockpath, self.sockpath)
570 os.symlink(self.realsockpath, self.sockpath)
571 except (OSError, socket.error), inst:
571 except (OSError, socket.error), inst:
572 try:
572 try:
573 os.unlink(self.realsockpath)
573 os.unlink(self.realsockpath)
574 except:
574 except:
575 pass
575 pass
576 os.rmdir(tempdir)
576 os.rmdir(tempdir)
577 if inst.errno == errno.EEXIST:
577 if inst.errno == errno.EEXIST:
578 raise AlreadyStartedException(_('could not start server: %s')
578 raise AlreadyStartedException(_('could not start server: %s')
579 % inst.strerror)
579 % inst.strerror)
580 raise
580 raise
581 else:
581 else:
582 raise
582 raise
583 self.sock.listen(5)
583 self.sock.listen(5)
584 self.fileno = self.sock.fileno
584 self.fileno = self.sock.fileno
585
585
586 def handle_timeout(self):
586 def handle_timeout(self):
587 pass
587 pass
588
588
589 def handle_event(self, fd, event):
589 def handle_event(self, fd, event):
590 sock, addr = self.sock.accept()
590 sock, addr = self.sock.accept()
591
591
592 cs = common.recvcs(sock)
592 cs = common.recvcs(sock)
593 version = ord(cs.read(1))
593 version = ord(cs.read(1))
594
594
595 sock.sendall(chr(common.version))
595 sock.sendall(chr(common.version))
596
596
597 if version != common.version:
597 if version != common.version:
598 self.ui.warn(_('received query from incompatible client '
598 self.ui.warn(_('received query from incompatible client '
599 'version %d\n') % version)
599 'version %d\n') % version)
600 return
600 return
601
601
602 names = cs.read().split('\0')
602 names = cs.read().split('\0')
603
603
604 states = names.pop()
604 states = names.pop()
605
605
606 self.ui.note(_('answering query for %r\n') % states)
606 self.ui.note(_('answering query for %r\n') % states)
607
607
608 if self.watcher.timeout:
608 if self.watcher.timeout:
609 # We got a query while a rescan is pending. Make sure we
609 # We got a query while a rescan is pending. Make sure we
610 # rescan before responding, or we could give back a wrong
610 # rescan before responding, or we could give back a wrong
611 # answer.
611 # answer.
612 self.watcher.handle_timeout()
612 self.watcher.handle_timeout()
613
613
614 if not names:
614 if not names:
615 def genresult(states, tree):
615 def genresult(states, tree):
616 for fn, state in self.watcher.walk(states, tree):
616 for fn, state in self.watcher.walk(states, tree):
617 yield fn
617 yield fn
618 else:
618 else:
619 def genresult(states, tree):
619 def genresult(states, tree):
620 for fn in names:
620 for fn in names:
621 l = self.watcher.lookup(fn, tree)
621 l = self.watcher.lookup(fn, tree)
622 try:
622 try:
623 if l in states:
623 if l in states:
624 yield fn
624 yield fn
625 except TypeError:
625 except TypeError:
626 for f, s in self.watcher.walk(states, l, fn):
626 for f, s in self.watcher.walk(states, l, fn):
627 yield f
627 yield f
628
628
629 results = ['\0'.join(r) for r in [
629 results = ['\0'.join(r) for r in [
630 genresult('l', self.watcher.statustrees['l']),
630 genresult('l', self.watcher.statustrees['l']),
631 genresult('m', self.watcher.statustrees['m']),
631 genresult('m', self.watcher.statustrees['m']),
632 genresult('a', self.watcher.statustrees['a']),
632 genresult('a', self.watcher.statustrees['a']),
633 genresult('r', self.watcher.statustrees['r']),
633 genresult('r', self.watcher.statustrees['r']),
634 genresult('!', self.watcher.statustrees['!']),
634 genresult('!', self.watcher.statustrees['!']),
635 '?' in states and genresult('?', self.watcher.statustrees['?']) or [],
635 '?' in states and genresult('?', self.watcher.statustrees['?']) or [],
636 [],
636 [],
637 'c' in states and genresult('n', self.watcher.tree) or [],
637 'c' in states and genresult('n', self.watcher.tree) or [],
638 ]]
638 ]]
639
639
640 try:
640 try:
641 try:
641 try:
642 sock.sendall(struct.pack(common.resphdrfmt,
642 sock.sendall(struct.pack(common.resphdrfmt,
643 *map(len, results)))
643 *map(len, results)))
644 sock.sendall(''.join(results))
644 sock.sendall(''.join(results))
645 finally:
645 finally:
646 sock.shutdown(socket.SHUT_WR)
646 sock.shutdown(socket.SHUT_WR)
647 except socket.error, err:
647 except socket.error, err:
648 if err[0] != errno.EPIPE:
648 if err[0] != errno.EPIPE:
649 raise
649 raise
650
650
651 def shutdown(self):
651 def shutdown(self):
652 self.sock.close()
652 self.sock.close()
653 try:
653 try:
654 os.unlink(self.sockpath)
654 os.unlink(self.sockpath)
655 if self.realsockpath:
655 if self.realsockpath:
656 os.unlink(self.realsockpath)
656 os.unlink(self.realsockpath)
657 os.rmdir(os.path.dirname(self.realsockpath))
657 os.rmdir(os.path.dirname(self.realsockpath))
658 except OSError, err:
658 except OSError, err:
659 if err.errno != errno.ENOENT:
659 if err.errno != errno.ENOENT:
660 raise
660 raise
661
661
662 class Master(object):
662 class Master(object):
663 def __init__(self, ui, repo, timeout=None):
663 def __init__(self, ui, repo, timeout=None):
664 self.ui = ui
664 self.ui = ui
665 self.repo = repo
665 self.repo = repo
666 self.poll = select.poll()
666 self.poll = select.poll()
667 self.watcher = Watcher(ui, repo, self)
667 self.watcher = Watcher(ui, repo, self)
668 self.server = Server(ui, repo, self.watcher, timeout)
668 self.server = Server(ui, repo, self.watcher, timeout)
669 self.table = {}
669 self.table = {}
670 for obj in (self.watcher, self.server):
670 for obj in (self.watcher, self.server):
671 fd = obj.fileno()
671 fd = obj.fileno()
672 self.table[fd] = obj
672 self.table[fd] = obj
673 self.poll.register(fd, obj.poll_events)
673 self.poll.register(fd, obj.poll_events)
674
674
675 def register(self, fd, mask):
675 def register(self, fd, mask):
676 self.poll.register(fd, mask)
676 self.poll.register(fd, mask)
677
677
678 def shutdown(self):
678 def shutdown(self):
679 for obj in self.table.itervalues():
679 for obj in self.table.itervalues():
680 obj.shutdown()
680 obj.shutdown()
681
681
682 def run(self):
682 def run(self):
683 self.watcher.setup()
683 self.watcher.setup()
684 self.ui.note(_('finished setup\n'))
684 self.ui.note(_('finished setup\n'))
685 if os.getenv('TIME_STARTUP'):
685 if os.getenv('TIME_STARTUP'):
686 sys.exit(0)
686 sys.exit(0)
687 while True:
687 while True:
688 timeout = None
688 timeout = None
689 timeobj = None
689 timeobj = None
690 for obj in self.table.itervalues():
690 for obj in self.table.itervalues():
691 if obj.timeout is not None and (timeout is None or obj.timeout < timeout):
691 if obj.timeout is not None and (timeout is None or obj.timeout < timeout):
692 timeout, timeobj = obj.timeout, obj
692 timeout, timeobj = obj.timeout, obj
693 try:
693 try:
694 if self.ui.debugflag:
694 if self.ui.debugflag:
695 if timeout is None:
695 if timeout is None:
696 self.ui.note(_('polling: no timeout\n'))
696 self.ui.note(_('polling: no timeout\n'))
697 else:
697 else:
698 self.ui.note(_('polling: %sms timeout\n') % timeout)
698 self.ui.note(_('polling: %sms timeout\n') % timeout)
699 events = self.poll.poll(timeout)
699 events = self.poll.poll(timeout)
700 except select.error, err:
700 except select.error, err:
701 if err[0] == errno.EINTR:
701 if err[0] == errno.EINTR:
702 continue
702 continue
703 raise
703 raise
704 if events:
704 if events:
705 for fd, event in events:
705 for fd, event in events:
706 self.table[fd].handle_event(fd, event)
706 self.table[fd].handle_event(fd, event)
707 elif timeobj:
707 elif timeobj:
708 timeobj.handle_timeout()
708 timeobj.handle_timeout()
709
709
710 def start(ui, repo):
710 def start(ui, repo):
711 m = Master(ui, repo)
711 m = Master(ui, repo)
712 sys.stdout.flush()
712 sys.stdout.flush()
713 sys.stderr.flush()
713 sys.stderr.flush()
714
714
715 pid = os.fork()
715 pid = os.fork()
716 if pid:
716 if pid:
717 return pid
717 return pid
718
718
719 os.setsid()
719 os.setsid()
720
720
721 fd = os.open('/dev/null', os.O_RDONLY)
721 fd = os.open('/dev/null', os.O_RDONLY)
722 os.dup2(fd, 0)
722 os.dup2(fd, 0)
723 if fd > 0:
723 if fd > 0:
724 os.close(fd)
724 os.close(fd)
725
725
726 fd = os.open(ui.config('inotify', 'log', '/dev/null'),
726 fd = os.open(ui.config('inotify', 'log', '/dev/null'),
727 os.O_RDWR | os.O_CREAT | os.O_TRUNC)
727 os.O_RDWR | os.O_CREAT | os.O_TRUNC)
728 os.dup2(fd, 1)
728 os.dup2(fd, 1)
729 os.dup2(fd, 2)
729 os.dup2(fd, 2)
730 if fd > 2:
730 if fd > 2:
731 os.close(fd)
731 os.close(fd)
732
732
733 try:
733 try:
734 m.run()
734 m.run()
735 finally:
735 finally:
736 m.shutdown()
736 m.shutdown()
737 os._exit(0)
737 os._exit(0)
General Comments 0
You need to be logged in to leave comments. Login now