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