##// END OF EJS Templates
inotify: fixup rebuilding ignore
Matt Mackall -
r7085:1fcc282e default
parent child Browse files
Show More
@@ -1,738 +1,738 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, oldstatus=None):
269 def updatestatus(self, wfn, st=None, status=None, oldstatus=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 if oldstatus is None:
276 if oldstatus is None:
277 oldstatus = d.get(fn)
277 oldstatus = d.get(fn)
278 isdir = False
278 isdir = False
279 if oldstatus:
279 if oldstatus:
280 try:
280 try:
281 if not status:
281 if not status:
282 if oldstatus in 'almn':
282 if oldstatus in 'almn':
283 status = '!'
283 status = '!'
284 elif oldstatus == 'r':
284 elif oldstatus == 'r':
285 status = 'r'
285 status = 'r'
286 except TypeError:
286 except TypeError:
287 # oldstatus may be a dict left behind by a deleted
287 # oldstatus may be a dict left behind by a deleted
288 # directory
288 # directory
289 isdir = True
289 isdir = True
290 else:
290 else:
291 if oldstatus in self.statuskeys and oldstatus != status:
291 if oldstatus in self.statuskeys and oldstatus != status:
292 del self.dir(self.statustrees[oldstatus], root)[fn]
292 del self.dir(self.statustrees[oldstatus], root)[fn]
293 if self.ui.debugflag and oldstatus != status:
293 if self.ui.debugflag and oldstatus != status:
294 if isdir:
294 if isdir:
295 self.ui.note(_('status: %r dir(%d) -> %s\n') %
295 self.ui.note(_('status: %r dir(%d) -> %s\n') %
296 (wfn, len(oldstatus), status))
296 (wfn, len(oldstatus), status))
297 else:
297 else:
298 self.ui.note(_('status: %r %s -> %s\n') %
298 self.ui.note(_('status: %r %s -> %s\n') %
299 (wfn, oldstatus, status))
299 (wfn, oldstatus, status))
300 if not isdir:
300 if not isdir:
301 if status and status != 'i':
301 if status and status != 'i':
302 d[fn] = status
302 d[fn] = status
303 if status in self.statuskeys:
303 if status in self.statuskeys:
304 dd = self.dir(self.statustrees[status], root)
304 dd = self.dir(self.statustrees[status], root)
305 if oldstatus != status or fn not in dd:
305 if oldstatus != status or fn not in dd:
306 dd[fn] = status
306 dd[fn] = status
307 else:
307 else:
308 d.pop(fn, None)
308 d.pop(fn, None)
309
309
310 def check_deleted(self, key):
310 def check_deleted(self, key):
311 # Files that had been deleted but were present in the dirstate
311 # Files that had been deleted but were present in the dirstate
312 # may have vanished from the dirstate; we must clean them up.
312 # may have vanished from the dirstate; we must clean them up.
313 nuke = []
313 nuke = []
314 for wfn, ignore in self.walk(key, self.statustrees[key]):
314 for wfn, ignore in self.walk(key, self.statustrees[key]):
315 if wfn not in self.repo.dirstate:
315 if wfn not in self.repo.dirstate:
316 nuke.append(wfn)
316 nuke.append(wfn)
317 for wfn in nuke:
317 for wfn in nuke:
318 root, fn = self.split(wfn)
318 root, fn = self.split(wfn)
319 del self.dir(self.statustrees[key], root)[fn]
319 del self.dir(self.statustrees[key], root)[fn]
320 del self.dir(self.tree, root)[fn]
320 del self.dir(self.tree, root)[fn]
321
321
322 def scan(self, topdir=''):
322 def scan(self, topdir=''):
323 self.handle_timeout()
323 self.handle_timeout()
324 ds = self.repo.dirstate._map.copy()
324 ds = self.repo.dirstate._map.copy()
325 self.add_watch(join(self.repo.root, topdir), self.mask)
325 self.add_watch(join(self.repo.root, topdir), self.mask)
326 for root, dirs, entries in walk(self.repo, topdir):
326 for root, dirs, entries in walk(self.repo, topdir):
327 for d in dirs:
327 for d in dirs:
328 self.add_watch(join(root, d), self.mask)
328 self.add_watch(join(root, d), self.mask)
329 wroot = root[len(self.wprefix):]
329 wroot = root[len(self.wprefix):]
330 d = self.dir(self.tree, wroot)
330 d = self.dir(self.tree, wroot)
331 for fn, kind in entries:
331 for fn, kind in entries:
332 wfn = join(wroot, fn)
332 wfn = join(wroot, fn)
333 self.updatestatus(wfn, self.getstat(wfn))
333 self.updatestatus(wfn, self.getstat(wfn))
334 ds.pop(wfn, None)
334 ds.pop(wfn, None)
335 wtopdir = topdir
335 wtopdir = topdir
336 if wtopdir and wtopdir[-1] != '/':
336 if wtopdir and wtopdir[-1] != '/':
337 wtopdir += '/'
337 wtopdir += '/'
338 for wfn, state in ds.iteritems():
338 for wfn, state in ds.iteritems():
339 if not wfn.startswith(wtopdir):
339 if not wfn.startswith(wtopdir):
340 continue
340 continue
341 status = state[0]
341 status = state[0]
342 st = self.getstat(wfn)
342 st = self.getstat(wfn)
343 if status == 'r' and not st:
343 if status == 'r' and not st:
344 self.updatestatus(wfn, st, status=status)
344 self.updatestatus(wfn, st, status=status)
345 else:
345 else:
346 self.updatestatus(wfn, st, oldstatus=status)
346 self.updatestatus(wfn, st, oldstatus=status)
347 self.check_deleted('!')
347 self.check_deleted('!')
348 self.check_deleted('r')
348 self.check_deleted('r')
349
349
350 def check_dirstate(self):
350 def check_dirstate(self):
351 ds_info = self.dirstate_info()
351 ds_info = self.dirstate_info()
352 if ds_info == self.ds_info:
352 if ds_info == self.ds_info:
353 return
353 return
354 self.ds_info = ds_info
354 self.ds_info = ds_info
355 if not self.ui.debugflag:
355 if not self.ui.debugflag:
356 self.last_event = None
356 self.last_event = None
357 self.ui.note(_('%s dirstate reload\n') % self.event_time())
357 self.ui.note(_('%s dirstate reload\n') % self.event_time())
358 self.repo.dirstate.invalidate()
358 self.repo.dirstate.invalidate()
359 self.scan()
359 self.scan()
360 self.ui.note(_('%s end dirstate reload\n') % self.event_time())
360 self.ui.note(_('%s end dirstate reload\n') % self.event_time())
361
361
362 def walk(self, states, tree, prefix=''):
362 def walk(self, states, tree, prefix=''):
363 # This is the "inner loop" when talking to the client.
363 # This is the "inner loop" when talking to the client.
364
364
365 for name, val in tree.iteritems():
365 for name, val in tree.iteritems():
366 path = join(prefix, name)
366 path = join(prefix, name)
367 try:
367 try:
368 if val in states:
368 if val in states:
369 yield path, val
369 yield path, val
370 except TypeError:
370 except TypeError:
371 for p in self.walk(states, val, path):
371 for p in self.walk(states, val, path):
372 yield p
372 yield p
373
373
374 def update_hgignore(self):
374 def update_hgignore(self):
375 # An update of the ignore file can potentially change the
375 # An update of the ignore file can potentially change the
376 # states of all unknown and ignored files.
376 # states of all unknown and ignored files.
377
377
378 # XXX If the user has other ignore files outside the repo, or
378 # XXX If the user has other ignore files outside the repo, or
379 # changes their list of ignore files at run time, we'll
379 # changes their list of ignore files at run time, we'll
380 # potentially never see changes to them. We could get the
380 # potentially never see changes to them. We could get the
381 # client to report to us what ignore data they're using.
381 # client to report to us what ignore data they're using.
382 # But it's easier to do nothing than to open that can of
382 # But it's easier to do nothing than to open that can of
383 # worms.
383 # worms.
384
384
385 if self.repo.dirstate.ignorefunc is not None:
385 if '_ignore' in self.repo.dirstate.__dict__:
386 self.repo.dirstate.ignorefunc = None
386 delattr(self.repo.dirstate, '_ignore')
387 self.ui.note(_('rescanning due to .hgignore change\n'))
387 self.ui.note(_('rescanning due to .hgignore change\n'))
388 self.scan()
388 self.scan()
389
389
390 def getstat(self, wpath):
390 def getstat(self, wpath):
391 try:
391 try:
392 return self.statcache[wpath]
392 return self.statcache[wpath]
393 except KeyError:
393 except KeyError:
394 try:
394 try:
395 return self.stat(wpath)
395 return self.stat(wpath)
396 except OSError, err:
396 except OSError, err:
397 if err.errno != errno.ENOENT:
397 if err.errno != errno.ENOENT:
398 raise
398 raise
399
399
400 def stat(self, wpath):
400 def stat(self, wpath):
401 try:
401 try:
402 st = os.lstat(join(self.wprefix, wpath))
402 st = os.lstat(join(self.wprefix, wpath))
403 ret = st.st_mode, st.st_size, st.st_mtime
403 ret = st.st_mode, st.st_size, st.st_mtime
404 self.statcache[wpath] = ret
404 self.statcache[wpath] = ret
405 return ret
405 return ret
406 except OSError, err:
406 except OSError, err:
407 self.statcache.pop(wpath, None)
407 self.statcache.pop(wpath, None)
408 raise
408 raise
409
409
410 def created(self, wpath):
410 def created(self, wpath):
411 if wpath == '.hgignore':
411 if wpath == '.hgignore':
412 self.update_hgignore()
412 self.update_hgignore()
413 try:
413 try:
414 st = self.stat(wpath)
414 st = self.stat(wpath)
415 if stat.S_ISREG(st[0]):
415 if stat.S_ISREG(st[0]):
416 self.updatestatus(wpath, st)
416 self.updatestatus(wpath, st)
417 except OSError, err:
417 except OSError, err:
418 pass
418 pass
419
419
420 def modified(self, wpath):
420 def modified(self, wpath):
421 if wpath == '.hgignore':
421 if wpath == '.hgignore':
422 self.update_hgignore()
422 self.update_hgignore()
423 try:
423 try:
424 st = self.stat(wpath)
424 st = self.stat(wpath)
425 if stat.S_ISREG(st[0]):
425 if stat.S_ISREG(st[0]):
426 if self.repo.dirstate[wpath] in 'lmn':
426 if self.repo.dirstate[wpath] in 'lmn':
427 self.updatestatus(wpath, st)
427 self.updatestatus(wpath, st)
428 except OSError:
428 except OSError:
429 pass
429 pass
430
430
431 def deleted(self, wpath):
431 def deleted(self, wpath):
432 if wpath == '.hgignore':
432 if wpath == '.hgignore':
433 self.update_hgignore()
433 self.update_hgignore()
434 elif wpath.startswith('.hg/'):
434 elif wpath.startswith('.hg/'):
435 if wpath == '.hg/wlock':
435 if wpath == '.hg/wlock':
436 self.check_dirstate()
436 self.check_dirstate()
437 return
437 return
438
438
439 self.updatestatus(wpath, None)
439 self.updatestatus(wpath, None)
440
440
441 def schedule_work(self, wpath, evt):
441 def schedule_work(self, wpath, evt):
442 self.eventq.setdefault(wpath, [])
442 self.eventq.setdefault(wpath, [])
443 prev = self.eventq[wpath]
443 prev = self.eventq[wpath]
444 try:
444 try:
445 if prev and evt == 'm' and prev[-1] in 'cm':
445 if prev and evt == 'm' and prev[-1] in 'cm':
446 return
446 return
447 self.eventq[wpath].append(evt)
447 self.eventq[wpath].append(evt)
448 finally:
448 finally:
449 self.deferred += 1
449 self.deferred += 1
450 self.timeout = 250
450 self.timeout = 250
451
451
452 def deferred_event(self, wpath, evt):
452 def deferred_event(self, wpath, evt):
453 if evt == 'c':
453 if evt == 'c':
454 self.created(wpath)
454 self.created(wpath)
455 elif evt == 'm':
455 elif evt == 'm':
456 self.modified(wpath)
456 self.modified(wpath)
457 elif evt == 'd':
457 elif evt == 'd':
458 self.deleted(wpath)
458 self.deleted(wpath)
459
459
460 def process_create(self, wpath, evt):
460 def process_create(self, wpath, evt):
461 if self.ui.debugflag:
461 if self.ui.debugflag:
462 self.ui.note(_('%s event: created %s\n') %
462 self.ui.note(_('%s event: created %s\n') %
463 (self.event_time(), wpath))
463 (self.event_time(), wpath))
464
464
465 if evt.mask & inotify.IN_ISDIR:
465 if evt.mask & inotify.IN_ISDIR:
466 self.scan(wpath)
466 self.scan(wpath)
467 else:
467 else:
468 self.schedule_work(wpath, 'c')
468 self.schedule_work(wpath, 'c')
469
469
470 def process_delete(self, wpath, evt):
470 def process_delete(self, wpath, evt):
471 if self.ui.debugflag:
471 if self.ui.debugflag:
472 self.ui.note(_('%s event: deleted %s\n') %
472 self.ui.note(_('%s event: deleted %s\n') %
473 (self.event_time(), wpath))
473 (self.event_time(), wpath))
474
474
475 if evt.mask & inotify.IN_ISDIR:
475 if evt.mask & inotify.IN_ISDIR:
476 self.scan(wpath)
476 self.scan(wpath)
477 else:
477 else:
478 self.schedule_work(wpath, 'd')
478 self.schedule_work(wpath, 'd')
479
479
480 def process_modify(self, wpath, evt):
480 def process_modify(self, wpath, evt):
481 if self.ui.debugflag:
481 if self.ui.debugflag:
482 self.ui.note(_('%s event: modified %s\n') %
482 self.ui.note(_('%s event: modified %s\n') %
483 (self.event_time(), wpath))
483 (self.event_time(), wpath))
484
484
485 if not (evt.mask & inotify.IN_ISDIR):
485 if not (evt.mask & inotify.IN_ISDIR):
486 self.schedule_work(wpath, 'm')
486 self.schedule_work(wpath, 'm')
487
487
488 def process_unmount(self, evt):
488 def process_unmount(self, evt):
489 self.ui.warn(_('filesystem containing %s was unmounted\n') %
489 self.ui.warn(_('filesystem containing %s was unmounted\n') %
490 evt.fullpath)
490 evt.fullpath)
491 sys.exit(0)
491 sys.exit(0)
492
492
493 def handle_event(self, fd, event):
493 def handle_event(self, fd, event):
494 if self.ui.debugflag:
494 if self.ui.debugflag:
495 self.ui.note(_('%s readable: %d bytes\n') %
495 self.ui.note(_('%s readable: %d bytes\n') %
496 (self.event_time(), self.threshold.readable()))
496 (self.event_time(), self.threshold.readable()))
497 if not self.threshold():
497 if not self.threshold():
498 if self.registered:
498 if self.registered:
499 if self.ui.debugflag:
499 if self.ui.debugflag:
500 self.ui.note(_('%s below threshold - unhooking\n') %
500 self.ui.note(_('%s below threshold - unhooking\n') %
501 (self.event_time()))
501 (self.event_time()))
502 self.master.poll.unregister(fd)
502 self.master.poll.unregister(fd)
503 self.registered = False
503 self.registered = False
504 self.timeout = 250
504 self.timeout = 250
505 else:
505 else:
506 self.read_events()
506 self.read_events()
507
507
508 def read_events(self, bufsize=None):
508 def read_events(self, bufsize=None):
509 events = self.watcher.read(bufsize)
509 events = self.watcher.read(bufsize)
510 if self.ui.debugflag:
510 if self.ui.debugflag:
511 self.ui.note(_('%s reading %d events\n') %
511 self.ui.note(_('%s reading %d events\n') %
512 (self.event_time(), len(events)))
512 (self.event_time(), len(events)))
513 for evt in events:
513 for evt in events:
514 wpath = self.wpath(evt)
514 wpath = self.wpath(evt)
515 if evt.mask & inotify.IN_UNMOUNT:
515 if evt.mask & inotify.IN_UNMOUNT:
516 self.process_unmount(wpath, evt)
516 self.process_unmount(wpath, evt)
517 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
517 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
518 self.process_modify(wpath, evt)
518 self.process_modify(wpath, evt)
519 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
519 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
520 inotify.IN_MOVED_FROM):
520 inotify.IN_MOVED_FROM):
521 self.process_delete(wpath, evt)
521 self.process_delete(wpath, evt)
522 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
522 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
523 self.process_create(wpath, evt)
523 self.process_create(wpath, evt)
524
524
525 def handle_timeout(self):
525 def handle_timeout(self):
526 if not self.registered:
526 if not self.registered:
527 if self.ui.debugflag:
527 if self.ui.debugflag:
528 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
528 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
529 (self.event_time(), self.threshold.readable()))
529 (self.event_time(), self.threshold.readable()))
530 self.read_events(0)
530 self.read_events(0)
531 self.master.poll.register(self, select.POLLIN)
531 self.master.poll.register(self, select.POLLIN)
532 self.registered = True
532 self.registered = True
533
533
534 if self.eventq:
534 if self.eventq:
535 if self.ui.debugflag:
535 if self.ui.debugflag:
536 self.ui.note(_('%s processing %d deferred events as %d\n') %
536 self.ui.note(_('%s processing %d deferred events as %d\n') %
537 (self.event_time(), self.deferred,
537 (self.event_time(), self.deferred,
538 len(self.eventq)))
538 len(self.eventq)))
539 for wpath, evts in util.sort(self.eventq.items()):
539 for wpath, evts in util.sort(self.eventq.items()):
540 for evt in evts:
540 for evt in evts:
541 self.deferred_event(wpath, evt)
541 self.deferred_event(wpath, evt)
542 self.eventq.clear()
542 self.eventq.clear()
543 self.deferred = 0
543 self.deferred = 0
544 self.timeout = None
544 self.timeout = None
545
545
546 def shutdown(self):
546 def shutdown(self):
547 self.watcher.close()
547 self.watcher.close()
548
548
549 class Server(object):
549 class Server(object):
550 poll_events = select.POLLIN
550 poll_events = select.POLLIN
551
551
552 def __init__(self, ui, repo, watcher, timeout):
552 def __init__(self, ui, repo, watcher, timeout):
553 self.ui = ui
553 self.ui = ui
554 self.repo = repo
554 self.repo = repo
555 self.watcher = watcher
555 self.watcher = watcher
556 self.timeout = timeout
556 self.timeout = timeout
557 self.sock = socket.socket(socket.AF_UNIX)
557 self.sock = socket.socket(socket.AF_UNIX)
558 self.sockpath = self.repo.join('inotify.sock')
558 self.sockpath = self.repo.join('inotify.sock')
559 self.realsockpath = None
559 self.realsockpath = None
560 try:
560 try:
561 self.sock.bind(self.sockpath)
561 self.sock.bind(self.sockpath)
562 except socket.error, err:
562 except socket.error, err:
563 if err[0] == errno.EADDRINUSE:
563 if err[0] == errno.EADDRINUSE:
564 raise AlreadyStartedException(_('could not start server: %s')
564 raise AlreadyStartedException(_('could not start server: %s')
565 % err[1])
565 % err[1])
566 if err[0] == "AF_UNIX path too long":
566 if err[0] == "AF_UNIX path too long":
567 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
567 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
568 self.realsockpath = os.path.join(tempdir, "inotify.sock")
568 self.realsockpath = os.path.join(tempdir, "inotify.sock")
569 try:
569 try:
570 self.sock.bind(self.realsockpath)
570 self.sock.bind(self.realsockpath)
571 os.symlink(self.realsockpath, self.sockpath)
571 os.symlink(self.realsockpath, self.sockpath)
572 except (OSError, socket.error), inst:
572 except (OSError, socket.error), inst:
573 try:
573 try:
574 os.unlink(self.realsockpath)
574 os.unlink(self.realsockpath)
575 except:
575 except:
576 pass
576 pass
577 os.rmdir(tempdir)
577 os.rmdir(tempdir)
578 if inst.errno == errno.EEXIST:
578 if inst.errno == errno.EEXIST:
579 raise AlreadyStartedException(_('could not start server: %s')
579 raise AlreadyStartedException(_('could not start server: %s')
580 % inst.strerror)
580 % inst.strerror)
581 raise
581 raise
582 else:
582 else:
583 raise
583 raise
584 self.sock.listen(5)
584 self.sock.listen(5)
585 self.fileno = self.sock.fileno
585 self.fileno = self.sock.fileno
586
586
587 def handle_timeout(self):
587 def handle_timeout(self):
588 pass
588 pass
589
589
590 def handle_event(self, fd, event):
590 def handle_event(self, fd, event):
591 sock, addr = self.sock.accept()
591 sock, addr = self.sock.accept()
592
592
593 cs = common.recvcs(sock)
593 cs = common.recvcs(sock)
594 version = ord(cs.read(1))
594 version = ord(cs.read(1))
595
595
596 sock.sendall(chr(common.version))
596 sock.sendall(chr(common.version))
597
597
598 if version != common.version:
598 if version != common.version:
599 self.ui.warn(_('received query from incompatible client '
599 self.ui.warn(_('received query from incompatible client '
600 'version %d\n') % version)
600 'version %d\n') % version)
601 return
601 return
602
602
603 names = cs.read().split('\0')
603 names = cs.read().split('\0')
604
604
605 states = names.pop()
605 states = names.pop()
606
606
607 self.ui.note(_('answering query for %r\n') % states)
607 self.ui.note(_('answering query for %r\n') % states)
608
608
609 if self.watcher.timeout:
609 if self.watcher.timeout:
610 # We got a query while a rescan is pending. Make sure we
610 # We got a query while a rescan is pending. Make sure we
611 # rescan before responding, or we could give back a wrong
611 # rescan before responding, or we could give back a wrong
612 # answer.
612 # answer.
613 self.watcher.handle_timeout()
613 self.watcher.handle_timeout()
614
614
615 if not names:
615 if not names:
616 def genresult(states, tree):
616 def genresult(states, tree):
617 for fn, state in self.watcher.walk(states, tree):
617 for fn, state in self.watcher.walk(states, tree):
618 yield fn
618 yield fn
619 else:
619 else:
620 def genresult(states, tree):
620 def genresult(states, tree):
621 for fn in names:
621 for fn in names:
622 l = self.watcher.lookup(fn, tree)
622 l = self.watcher.lookup(fn, tree)
623 try:
623 try:
624 if l in states:
624 if l in states:
625 yield fn
625 yield fn
626 except TypeError:
626 except TypeError:
627 for f, s in self.watcher.walk(states, l, fn):
627 for f, s in self.watcher.walk(states, l, fn):
628 yield f
628 yield f
629
629
630 results = ['\0'.join(r) for r in [
630 results = ['\0'.join(r) for r in [
631 genresult('l', self.watcher.statustrees['l']),
631 genresult('l', self.watcher.statustrees['l']),
632 genresult('m', self.watcher.statustrees['m']),
632 genresult('m', self.watcher.statustrees['m']),
633 genresult('a', self.watcher.statustrees['a']),
633 genresult('a', self.watcher.statustrees['a']),
634 genresult('r', self.watcher.statustrees['r']),
634 genresult('r', self.watcher.statustrees['r']),
635 genresult('!', self.watcher.statustrees['!']),
635 genresult('!', self.watcher.statustrees['!']),
636 '?' in states and genresult('?', self.watcher.statustrees['?']) or [],
636 '?' in states and genresult('?', self.watcher.statustrees['?']) or [],
637 [],
637 [],
638 'c' in states and genresult('n', self.watcher.tree) or [],
638 'c' in states and genresult('n', self.watcher.tree) or [],
639 ]]
639 ]]
640
640
641 try:
641 try:
642 try:
642 try:
643 sock.sendall(struct.pack(common.resphdrfmt,
643 sock.sendall(struct.pack(common.resphdrfmt,
644 *map(len, results)))
644 *map(len, results)))
645 sock.sendall(''.join(results))
645 sock.sendall(''.join(results))
646 finally:
646 finally:
647 sock.shutdown(socket.SHUT_WR)
647 sock.shutdown(socket.SHUT_WR)
648 except socket.error, err:
648 except socket.error, err:
649 if err[0] != errno.EPIPE:
649 if err[0] != errno.EPIPE:
650 raise
650 raise
651
651
652 def shutdown(self):
652 def shutdown(self):
653 self.sock.close()
653 self.sock.close()
654 try:
654 try:
655 os.unlink(self.sockpath)
655 os.unlink(self.sockpath)
656 if self.realsockpath:
656 if self.realsockpath:
657 os.unlink(self.realsockpath)
657 os.unlink(self.realsockpath)
658 os.rmdir(os.path.dirname(self.realsockpath))
658 os.rmdir(os.path.dirname(self.realsockpath))
659 except OSError, err:
659 except OSError, err:
660 if err.errno != errno.ENOENT:
660 if err.errno != errno.ENOENT:
661 raise
661 raise
662
662
663 class Master(object):
663 class Master(object):
664 def __init__(self, ui, repo, timeout=None):
664 def __init__(self, ui, repo, timeout=None):
665 self.ui = ui
665 self.ui = ui
666 self.repo = repo
666 self.repo = repo
667 self.poll = select.poll()
667 self.poll = select.poll()
668 self.watcher = Watcher(ui, repo, self)
668 self.watcher = Watcher(ui, repo, self)
669 self.server = Server(ui, repo, self.watcher, timeout)
669 self.server = Server(ui, repo, self.watcher, timeout)
670 self.table = {}
670 self.table = {}
671 for obj in (self.watcher, self.server):
671 for obj in (self.watcher, self.server):
672 fd = obj.fileno()
672 fd = obj.fileno()
673 self.table[fd] = obj
673 self.table[fd] = obj
674 self.poll.register(fd, obj.poll_events)
674 self.poll.register(fd, obj.poll_events)
675
675
676 def register(self, fd, mask):
676 def register(self, fd, mask):
677 self.poll.register(fd, mask)
677 self.poll.register(fd, mask)
678
678
679 def shutdown(self):
679 def shutdown(self):
680 for obj in self.table.itervalues():
680 for obj in self.table.itervalues():
681 obj.shutdown()
681 obj.shutdown()
682
682
683 def run(self):
683 def run(self):
684 self.watcher.setup()
684 self.watcher.setup()
685 self.ui.note(_('finished setup\n'))
685 self.ui.note(_('finished setup\n'))
686 if os.getenv('TIME_STARTUP'):
686 if os.getenv('TIME_STARTUP'):
687 sys.exit(0)
687 sys.exit(0)
688 while True:
688 while True:
689 timeout = None
689 timeout = None
690 timeobj = None
690 timeobj = None
691 for obj in self.table.itervalues():
691 for obj in self.table.itervalues():
692 if obj.timeout is not None and (timeout is None or obj.timeout < timeout):
692 if obj.timeout is not None and (timeout is None or obj.timeout < timeout):
693 timeout, timeobj = obj.timeout, obj
693 timeout, timeobj = obj.timeout, obj
694 try:
694 try:
695 if self.ui.debugflag:
695 if self.ui.debugflag:
696 if timeout is None:
696 if timeout is None:
697 self.ui.note(_('polling: no timeout\n'))
697 self.ui.note(_('polling: no timeout\n'))
698 else:
698 else:
699 self.ui.note(_('polling: %sms timeout\n') % timeout)
699 self.ui.note(_('polling: %sms timeout\n') % timeout)
700 events = self.poll.poll(timeout)
700 events = self.poll.poll(timeout)
701 except select.error, err:
701 except select.error, err:
702 if err[0] == errno.EINTR:
702 if err[0] == errno.EINTR:
703 continue
703 continue
704 raise
704 raise
705 if events:
705 if events:
706 for fd, event in events:
706 for fd, event in events:
707 self.table[fd].handle_event(fd, event)
707 self.table[fd].handle_event(fd, event)
708 elif timeobj:
708 elif timeobj:
709 timeobj.handle_timeout()
709 timeobj.handle_timeout()
710
710
711 def start(ui, repo):
711 def start(ui, repo):
712 m = Master(ui, repo)
712 m = Master(ui, repo)
713 sys.stdout.flush()
713 sys.stdout.flush()
714 sys.stderr.flush()
714 sys.stderr.flush()
715
715
716 pid = os.fork()
716 pid = os.fork()
717 if pid:
717 if pid:
718 return pid
718 return pid
719
719
720 os.setsid()
720 os.setsid()
721
721
722 fd = os.open('/dev/null', os.O_RDONLY)
722 fd = os.open('/dev/null', os.O_RDONLY)
723 os.dup2(fd, 0)
723 os.dup2(fd, 0)
724 if fd > 0:
724 if fd > 0:
725 os.close(fd)
725 os.close(fd)
726
726
727 fd = os.open(ui.config('inotify', 'log', '/dev/null'),
727 fd = os.open(ui.config('inotify', 'log', '/dev/null'),
728 os.O_RDWR | os.O_CREAT | os.O_TRUNC)
728 os.O_RDWR | os.O_CREAT | os.O_TRUNC)
729 os.dup2(fd, 1)
729 os.dup2(fd, 1)
730 os.dup2(fd, 2)
730 os.dup2(fd, 2)
731 if fd > 2:
731 if fd > 2:
732 os.close(fd)
732 os.close(fd)
733
733
734 try:
734 try:
735 m.run()
735 m.run()
736 finally:
736 finally:
737 m.shutdown()
737 m.shutdown()
738 os._exit(0)
738 os._exit(0)
General Comments 0
You need to be logged in to leave comments. Login now