##// END OF EJS Templates
inotify: make mask a class variable since it's instance-independant
Nicolas Dumazet -
r8383:dcfdcb51 default
parent child Browse files
Show More
@@ -1,759 +1,759 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 = (
120 inotify.IN_ATTRIB |
121 inotify.IN_CREATE |
122 inotify.IN_DELETE |
123 inotify.IN_DELETE_SELF |
124 inotify.IN_MODIFY |
125 inotify.IN_MOVED_FROM |
126 inotify.IN_MOVED_TO |
127 inotify.IN_MOVE_SELF |
128 inotify.IN_ONLYDIR |
129 inotify.IN_UNMOUNT |
130 0)
119
131
120 def __init__(self, ui, repo, master):
132 def __init__(self, ui, repo, master):
121 self.ui = ui
133 self.ui = ui
122 self.repo = repo
134 self.repo = repo
123 self.wprefix = self.repo.wjoin('')
135 self.wprefix = self.repo.wjoin('')
124 self.timeout = None
136 self.timeout = None
125 self.master = master
137 self.master = master
126 self.mask = (
127 inotify.IN_ATTRIB |
128 inotify.IN_CREATE |
129 inotify.IN_DELETE |
130 inotify.IN_DELETE_SELF |
131 inotify.IN_MODIFY |
132 inotify.IN_MOVED_FROM |
133 inotify.IN_MOVED_TO |
134 inotify.IN_MOVE_SELF |
135 inotify.IN_ONLYDIR |
136 inotify.IN_UNMOUNT |
137 0)
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.repo.dirstate.__class__.inotifyserver = True
147 self.repo.dirstate.__class__.inotifyserver = True
148
148
149 self.tree = {}
149 self.tree = {}
150 self.statcache = {}
150 self.statcache = {}
151 self.statustrees = dict([(s, {}) for s in self.statuskeys])
151 self.statustrees = dict([(s, {}) for s in self.statuskeys])
152
152
153 self.watches = 0
153 self.watches = 0
154 self.last_event = None
154 self.last_event = None
155
155
156 self.eventq = {}
156 self.eventq = {}
157 self.deferred = 0
157 self.deferred = 0
158
158
159 self.ds_info = self.dirstate_info()
159 self.ds_info = self.dirstate_info()
160 self.scan()
160 self.scan()
161
161
162 def event_time(self):
162 def event_time(self):
163 last = self.last_event
163 last = self.last_event
164 now = time.time()
164 now = time.time()
165 self.last_event = now
165 self.last_event = now
166
166
167 if last is None:
167 if last is None:
168 return 'start'
168 return 'start'
169 delta = now - last
169 delta = now - last
170 if delta < 5:
170 if delta < 5:
171 return '+%.3f' % delta
171 return '+%.3f' % delta
172 if delta < 50:
172 if delta < 50:
173 return '+%.2f' % delta
173 return '+%.2f' % delta
174 return '+%.1f' % delta
174 return '+%.1f' % delta
175
175
176 def dirstate_info(self):
176 def dirstate_info(self):
177 try:
177 try:
178 st = os.lstat(self.repo.join('dirstate'))
178 st = os.lstat(self.repo.join('dirstate'))
179 return st.st_mtime, st.st_ino
179 return st.st_mtime, st.st_ino
180 except OSError, err:
180 except OSError, err:
181 if err.errno != errno.ENOENT:
181 if err.errno != errno.ENOENT:
182 raise
182 raise
183 return 0, 0
183 return 0, 0
184
184
185 def add_watch(self, path, mask):
185 def add_watch(self, path, mask):
186 if not path:
186 if not path:
187 return
187 return
188 if self.watcher.path(path) is None:
188 if self.watcher.path(path) is None:
189 if self.ui.debugflag:
189 if self.ui.debugflag:
190 self.ui.note(_('watching %r\n') % path[len(self.wprefix):])
190 self.ui.note(_('watching %r\n') % path[len(self.wprefix):])
191 try:
191 try:
192 self.watcher.add(path, mask)
192 self.watcher.add(path, mask)
193 self.watches += 1
193 self.watches += 1
194 except OSError, err:
194 except OSError, err:
195 if err.errno in (errno.ENOENT, errno.ENOTDIR):
195 if err.errno in (errno.ENOENT, errno.ENOTDIR):
196 return
196 return
197 if err.errno != errno.ENOSPC:
197 if err.errno != errno.ENOSPC:
198 raise
198 raise
199 _explain_watch_limit(self.ui, self.repo, self.watches)
199 _explain_watch_limit(self.ui, self.repo, self.watches)
200
200
201 def setup(self):
201 def setup(self):
202 self.ui.note(_('watching directories under %r\n') % self.repo.root)
202 self.ui.note(_('watching directories under %r\n') % self.repo.root)
203 self.add_watch(self.repo.path, inotify.IN_DELETE)
203 self.add_watch(self.repo.path, inotify.IN_DELETE)
204 self.check_dirstate()
204 self.check_dirstate()
205
205
206 def wpath(self, evt):
206 def wpath(self, evt):
207 path = evt.fullpath
207 path = evt.fullpath
208 if path == self.repo.root:
208 if path == self.repo.root:
209 return ''
209 return ''
210 if path.startswith(self.wprefix):
210 if path.startswith(self.wprefix):
211 return path[len(self.wprefix):]
211 return path[len(self.wprefix):]
212 raise 'wtf? ' + path
212 raise 'wtf? ' + path
213
213
214 def dir(self, tree, path):
214 def dir(self, tree, path):
215 if path:
215 if path:
216 for name in path.split('/'):
216 for name in path.split('/'):
217 tree.setdefault(name, {})
217 tree.setdefault(name, {})
218 tree = tree[name]
218 tree = tree[name]
219 return tree
219 return tree
220
220
221 def lookup(self, path, tree):
221 def lookup(self, path, tree):
222 if path:
222 if path:
223 try:
223 try:
224 for name in path.split('/'):
224 for name in path.split('/'):
225 tree = tree[name]
225 tree = tree[name]
226 except KeyError:
226 except KeyError:
227 return 'x'
227 return 'x'
228 except TypeError:
228 except TypeError:
229 return 'd'
229 return 'd'
230 return tree
230 return tree
231
231
232 def split(self, path):
232 def split(self, path):
233 c = path.rfind('/')
233 c = path.rfind('/')
234 if c == -1:
234 if c == -1:
235 return '', path
235 return '', path
236 return path[:c], path[c+1:]
236 return path[:c], path[c+1:]
237
237
238 def filestatus(self, fn, st):
238 def filestatus(self, fn, st):
239 try:
239 try:
240 type_, mode, size, time = self.repo.dirstate._map[fn][:4]
240 type_, mode, size, time = self.repo.dirstate._map[fn][:4]
241 except KeyError:
241 except KeyError:
242 type_ = '?'
242 type_ = '?'
243 if type_ == 'n':
243 if type_ == 'n':
244 st_mode, st_size, st_mtime = st
244 st_mode, st_size, st_mtime = st
245 if size == -1:
245 if size == -1:
246 return 'l'
246 return 'l'
247 if size and (size != st_size or (mode ^ st_mode) & 0100):
247 if size and (size != st_size or (mode ^ st_mode) & 0100):
248 return 'm'
248 return 'm'
249 if time != int(st_mtime):
249 if time != int(st_mtime):
250 return 'l'
250 return 'l'
251 return 'n'
251 return 'n'
252 if type_ == '?' and self.repo.dirstate._ignore(fn):
252 if type_ == '?' and self.repo.dirstate._ignore(fn):
253 return 'i'
253 return 'i'
254 return type_
254 return type_
255
255
256 def updatestatus(self, wfn, osstat=None, newstatus=None):
256 def updatestatus(self, wfn, osstat=None, newstatus=None):
257 '''
257 '''
258 Update the stored status of a file or directory.
258 Update the stored status of a file or directory.
259
259
260 osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
260 osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
261
261
262 newstatus: char in statuskeys, new status to apply.
262 newstatus: char in statuskeys, new status to apply.
263 '''
263 '''
264 if osstat:
264 if osstat:
265 newstatus = self.filestatus(wfn, osstat)
265 newstatus = self.filestatus(wfn, osstat)
266 else:
266 else:
267 self.statcache.pop(wfn, None)
267 self.statcache.pop(wfn, None)
268 root, fn = self.split(wfn)
268 root, fn = self.split(wfn)
269 d = self.dir(self.tree, root)
269 d = self.dir(self.tree, root)
270 oldstatus = d.get(fn)
270 oldstatus = d.get(fn)
271 isdir = False
271 isdir = False
272 if oldstatus:
272 if oldstatus:
273 try:
273 try:
274 if not newstatus:
274 if not newstatus:
275 if oldstatus in 'almn':
275 if oldstatus in 'almn':
276 newstatus = '!'
276 newstatus = '!'
277 elif oldstatus == 'r':
277 elif oldstatus == 'r':
278 newstatus = 'r'
278 newstatus = 'r'
279 except TypeError:
279 except TypeError:
280 # oldstatus may be a dict left behind by a deleted
280 # oldstatus may be a dict left behind by a deleted
281 # directory
281 # directory
282 isdir = True
282 isdir = True
283 else:
283 else:
284 if oldstatus in self.statuskeys and oldstatus != newstatus:
284 if oldstatus in self.statuskeys and oldstatus != newstatus:
285 del self.dir(self.statustrees[oldstatus], root)[fn]
285 del self.dir(self.statustrees[oldstatus], root)[fn]
286 if self.ui.debugflag and oldstatus != newstatus:
286 if self.ui.debugflag and oldstatus != newstatus:
287 if isdir:
287 if isdir:
288 self.ui.note(_('status: %r dir(%d) -> %s\n') %
288 self.ui.note(_('status: %r dir(%d) -> %s\n') %
289 (wfn, len(oldstatus), newstatus))
289 (wfn, len(oldstatus), newstatus))
290 else:
290 else:
291 self.ui.note(_('status: %r %s -> %s\n') %
291 self.ui.note(_('status: %r %s -> %s\n') %
292 (wfn, oldstatus, newstatus))
292 (wfn, oldstatus, newstatus))
293 if not isdir:
293 if not isdir:
294 if newstatus and newstatus != 'i':
294 if newstatus and newstatus != 'i':
295 d[fn] = newstatus
295 d[fn] = newstatus
296 if newstatus in self.statuskeys:
296 if newstatus in self.statuskeys:
297 dd = self.dir(self.statustrees[newstatus], root)
297 dd = self.dir(self.statustrees[newstatus], root)
298 if oldstatus != newstatus or fn not in dd:
298 if oldstatus != newstatus or fn not in dd:
299 dd[fn] = newstatus
299 dd[fn] = newstatus
300 else:
300 else:
301 d.pop(fn, None)
301 d.pop(fn, None)
302 elif not newstatus:
302 elif not newstatus:
303 # a directory is being removed, check its contents
303 # a directory is being removed, check its contents
304 for subfile, b in oldstatus.copy().iteritems():
304 for subfile, b in oldstatus.copy().iteritems():
305 self.updatestatus(wfn + '/' + subfile, None)
305 self.updatestatus(wfn + '/' + subfile, None)
306
306
307
307
308 def check_deleted(self, key):
308 def check_deleted(self, key):
309 # Files that had been deleted but were present in the dirstate
309 # Files that had been deleted but were present in the dirstate
310 # may have vanished from the dirstate; we must clean them up.
310 # may have vanished from the dirstate; we must clean them up.
311 nuke = []
311 nuke = []
312 for wfn, ignore in self.walk(key, self.statustrees[key]):
312 for wfn, ignore in self.walk(key, self.statustrees[key]):
313 if wfn not in self.repo.dirstate:
313 if wfn not in self.repo.dirstate:
314 nuke.append(wfn)
314 nuke.append(wfn)
315 for wfn in nuke:
315 for wfn in nuke:
316 root, fn = self.split(wfn)
316 root, fn = self.split(wfn)
317 del self.dir(self.statustrees[key], root)[fn]
317 del self.dir(self.statustrees[key], root)[fn]
318 del self.dir(self.tree, root)[fn]
318 del self.dir(self.tree, root)[fn]
319
319
320 def scan(self, topdir=''):
320 def scan(self, topdir=''):
321 self.handle_timeout()
321 self.handle_timeout()
322 ds = self.repo.dirstate._map.copy()
322 ds = self.repo.dirstate._map.copy()
323 self.add_watch(join(self.repo.root, topdir), self.mask)
323 self.add_watch(join(self.repo.root, topdir), self.mask)
324 for root, dirs, files in walk(self.repo, topdir):
324 for root, dirs, files in walk(self.repo, topdir):
325 for d in dirs:
325 for d in dirs:
326 self.add_watch(join(root, d), self.mask)
326 self.add_watch(join(root, d), self.mask)
327 wroot = root[len(self.wprefix):]
327 wroot = root[len(self.wprefix):]
328 d = self.dir(self.tree, wroot)
328 d = self.dir(self.tree, wroot)
329 for fn in files:
329 for fn in files:
330 wfn = join(wroot, fn)
330 wfn = join(wroot, fn)
331 self.updatestatus(wfn, self.getstat(wfn))
331 self.updatestatus(wfn, self.getstat(wfn))
332 ds.pop(wfn, None)
332 ds.pop(wfn, None)
333 wtopdir = topdir
333 wtopdir = topdir
334 if wtopdir and wtopdir[-1] != '/':
334 if wtopdir and wtopdir[-1] != '/':
335 wtopdir += '/'
335 wtopdir += '/'
336 for wfn, state in ds.iteritems():
336 for wfn, state in ds.iteritems():
337 if not wfn.startswith(wtopdir):
337 if not wfn.startswith(wtopdir):
338 continue
338 continue
339 try:
339 try:
340 st = self.stat(wfn)
340 st = self.stat(wfn)
341 except OSError:
341 except OSError:
342 status = state[0]
342 status = state[0]
343 self.updatestatus(wfn, None, newstatus=status)
343 self.updatestatus(wfn, None, newstatus=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:
405 except OSError:
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:
416 except OSError:
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 self.schedule_work(wpath, 'd')
476 self.schedule_work(wpath, 'd')
477
477
478 def process_modify(self, wpath, evt):
478 def process_modify(self, wpath, evt):
479 if self.ui.debugflag:
479 if self.ui.debugflag:
480 self.ui.note(_('%s event: modified %s\n') %
480 self.ui.note(_('%s event: modified %s\n') %
481 (self.event_time(), wpath))
481 (self.event_time(), wpath))
482
482
483 if not (evt.mask & inotify.IN_ISDIR):
483 if not (evt.mask & inotify.IN_ISDIR):
484 self.schedule_work(wpath, 'm')
484 self.schedule_work(wpath, 'm')
485
485
486 def process_unmount(self, evt):
486 def process_unmount(self, evt):
487 self.ui.warn(_('filesystem containing %s was unmounted\n') %
487 self.ui.warn(_('filesystem containing %s was unmounted\n') %
488 evt.fullpath)
488 evt.fullpath)
489 sys.exit(0)
489 sys.exit(0)
490
490
491 def handle_event(self, fd, event):
491 def handle_event(self, fd, event):
492 if self.ui.debugflag:
492 if self.ui.debugflag:
493 self.ui.note(_('%s readable: %d bytes\n') %
493 self.ui.note(_('%s readable: %d bytes\n') %
494 (self.event_time(), self.threshold.readable()))
494 (self.event_time(), self.threshold.readable()))
495 if not self.threshold():
495 if not self.threshold():
496 if self.registered:
496 if self.registered:
497 if self.ui.debugflag:
497 if self.ui.debugflag:
498 self.ui.note(_('%s below threshold - unhooking\n') %
498 self.ui.note(_('%s below threshold - unhooking\n') %
499 (self.event_time()))
499 (self.event_time()))
500 self.master.poll.unregister(fd)
500 self.master.poll.unregister(fd)
501 self.registered = False
501 self.registered = False
502 self.timeout = 250
502 self.timeout = 250
503 else:
503 else:
504 self.read_events()
504 self.read_events()
505
505
506 def read_events(self, bufsize=None):
506 def read_events(self, bufsize=None):
507 events = self.watcher.read(bufsize)
507 events = self.watcher.read(bufsize)
508 if self.ui.debugflag:
508 if self.ui.debugflag:
509 self.ui.note(_('%s reading %d events\n') %
509 self.ui.note(_('%s reading %d events\n') %
510 (self.event_time(), len(events)))
510 (self.event_time(), len(events)))
511 for evt in events:
511 for evt in events:
512 wpath = self.wpath(evt)
512 wpath = self.wpath(evt)
513 if evt.mask & inotify.IN_UNMOUNT:
513 if evt.mask & inotify.IN_UNMOUNT:
514 self.process_unmount(wpath, evt)
514 self.process_unmount(wpath, evt)
515 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
515 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
516 self.process_modify(wpath, evt)
516 self.process_modify(wpath, evt)
517 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
517 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
518 inotify.IN_MOVED_FROM):
518 inotify.IN_MOVED_FROM):
519 self.process_delete(wpath, evt)
519 self.process_delete(wpath, evt)
520 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
520 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
521 self.process_create(wpath, evt)
521 self.process_create(wpath, evt)
522
522
523 def handle_timeout(self):
523 def handle_timeout(self):
524 if not self.registered:
524 if not self.registered:
525 if self.ui.debugflag:
525 if self.ui.debugflag:
526 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
526 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
527 (self.event_time(), self.threshold.readable()))
527 (self.event_time(), self.threshold.readable()))
528 self.read_events(0)
528 self.read_events(0)
529 self.master.poll.register(self, select.POLLIN)
529 self.master.poll.register(self, select.POLLIN)
530 self.registered = True
530 self.registered = True
531
531
532 if self.eventq:
532 if self.eventq:
533 if self.ui.debugflag:
533 if self.ui.debugflag:
534 self.ui.note(_('%s processing %d deferred events as %d\n') %
534 self.ui.note(_('%s processing %d deferred events as %d\n') %
535 (self.event_time(), self.deferred,
535 (self.event_time(), self.deferred,
536 len(self.eventq)))
536 len(self.eventq)))
537 for wpath, evts in sorted(self.eventq.iteritems()):
537 for wpath, evts in sorted(self.eventq.iteritems()):
538 for evt in evts:
538 for evt in evts:
539 self.deferred_event(wpath, evt)
539 self.deferred_event(wpath, evt)
540 self.eventq.clear()
540 self.eventq.clear()
541 self.deferred = 0
541 self.deferred = 0
542 self.timeout = None
542 self.timeout = None
543
543
544 def shutdown(self):
544 def shutdown(self):
545 self.watcher.close()
545 self.watcher.close()
546
546
547 class Server(object):
547 class Server(object):
548 poll_events = select.POLLIN
548 poll_events = select.POLLIN
549
549
550 def __init__(self, ui, repo, repowatcher, timeout):
550 def __init__(self, ui, repo, repowatcher, timeout):
551 self.ui = ui
551 self.ui = ui
552 self.repo = repo
552 self.repo = repo
553 self.repowatcher = repowatcher
553 self.repowatcher = repowatcher
554 self.timeout = timeout
554 self.timeout = timeout
555 self.sock = socket.socket(socket.AF_UNIX)
555 self.sock = socket.socket(socket.AF_UNIX)
556 self.sockpath = self.repo.join('inotify.sock')
556 self.sockpath = self.repo.join('inotify.sock')
557 self.realsockpath = None
557 self.realsockpath = None
558 try:
558 try:
559 self.sock.bind(self.sockpath)
559 self.sock.bind(self.sockpath)
560 except socket.error, err:
560 except socket.error, err:
561 if err[0] == errno.EADDRINUSE:
561 if err[0] == errno.EADDRINUSE:
562 raise AlreadyStartedException(_('could not start server: %s')
562 raise AlreadyStartedException(_('could not start server: %s')
563 % err[1])
563 % err[1])
564 if err[0] == "AF_UNIX path too long":
564 if err[0] == "AF_UNIX path too long":
565 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
565 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
566 self.realsockpath = os.path.join(tempdir, "inotify.sock")
566 self.realsockpath = os.path.join(tempdir, "inotify.sock")
567 try:
567 try:
568 self.sock.bind(self.realsockpath)
568 self.sock.bind(self.realsockpath)
569 os.symlink(self.realsockpath, self.sockpath)
569 os.symlink(self.realsockpath, self.sockpath)
570 except (OSError, socket.error), inst:
570 except (OSError, socket.error), inst:
571 try:
571 try:
572 os.unlink(self.realsockpath)
572 os.unlink(self.realsockpath)
573 except:
573 except:
574 pass
574 pass
575 os.rmdir(tempdir)
575 os.rmdir(tempdir)
576 if inst.errno == errno.EEXIST:
576 if inst.errno == errno.EEXIST:
577 raise AlreadyStartedException(_('could not start server: %s')
577 raise AlreadyStartedException(_('could not start server: %s')
578 % inst.strerror)
578 % inst.strerror)
579 raise
579 raise
580 else:
580 else:
581 raise
581 raise
582 self.sock.listen(5)
582 self.sock.listen(5)
583 self.fileno = self.sock.fileno
583 self.fileno = self.sock.fileno
584
584
585 def handle_timeout(self):
585 def handle_timeout(self):
586 pass
586 pass
587
587
588 def handle_event(self, fd, event):
588 def handle_event(self, fd, event):
589 sock, addr = self.sock.accept()
589 sock, addr = self.sock.accept()
590
590
591 cs = common.recvcs(sock)
591 cs = common.recvcs(sock)
592 version = ord(cs.read(1))
592 version = ord(cs.read(1))
593
593
594 sock.sendall(chr(common.version))
594 sock.sendall(chr(common.version))
595
595
596 if version != common.version:
596 if version != common.version:
597 self.ui.warn(_('received query from incompatible client '
597 self.ui.warn(_('received query from incompatible client '
598 'version %d\n') % version)
598 'version %d\n') % version)
599 return
599 return
600
600
601 names = cs.read().split('\0')
601 names = cs.read().split('\0')
602
602
603 states = names.pop()
603 states = names.pop()
604
604
605 self.ui.note(_('answering query for %r\n') % states)
605 self.ui.note(_('answering query for %r\n') % states)
606
606
607 if self.repowatcher.timeout:
607 if self.repowatcher.timeout:
608 # We got a query while a rescan is pending. Make sure we
608 # We got a query while a rescan is pending. Make sure we
609 # rescan before responding, or we could give back a wrong
609 # rescan before responding, or we could give back a wrong
610 # answer.
610 # answer.
611 self.repowatcher.handle_timeout()
611 self.repowatcher.handle_timeout()
612
612
613 if not names:
613 if not names:
614 def genresult(states, tree):
614 def genresult(states, tree):
615 for fn, state in self.repowatcher.walk(states, tree):
615 for fn, state in self.repowatcher.walk(states, tree):
616 yield fn
616 yield fn
617 else:
617 else:
618 def genresult(states, tree):
618 def genresult(states, tree):
619 for fn in names:
619 for fn in names:
620 l = self.repowatcher.lookup(fn, tree)
620 l = self.repowatcher.lookup(fn, tree)
621 try:
621 try:
622 if l in states:
622 if l in states:
623 yield fn
623 yield fn
624 except TypeError:
624 except TypeError:
625 for f, s in self.repowatcher.walk(states, l, fn):
625 for f, s in self.repowatcher.walk(states, l, fn):
626 yield f
626 yield f
627
627
628 results = ['\0'.join(r) for r in [
628 results = ['\0'.join(r) for r in [
629 genresult('l', self.repowatcher.statustrees['l']),
629 genresult('l', self.repowatcher.statustrees['l']),
630 genresult('m', self.repowatcher.statustrees['m']),
630 genresult('m', self.repowatcher.statustrees['m']),
631 genresult('a', self.repowatcher.statustrees['a']),
631 genresult('a', self.repowatcher.statustrees['a']),
632 genresult('r', self.repowatcher.statustrees['r']),
632 genresult('r', self.repowatcher.statustrees['r']),
633 genresult('!', self.repowatcher.statustrees['!']),
633 genresult('!', self.repowatcher.statustrees['!']),
634 '?' in states
634 '?' in states
635 and genresult('?', self.repowatcher.statustrees['?'])
635 and genresult('?', self.repowatcher.statustrees['?'])
636 or [],
636 or [],
637 [],
637 [],
638 'c' in states and genresult('n', self.repowatcher.tree) or [],
638 'c' in states and genresult('n', self.repowatcher.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.repowatcher = RepoWatcher(ui, repo, self)
668 self.repowatcher = RepoWatcher(ui, repo, self)
669 self.server = Server(ui, repo, self.repowatcher, timeout)
669 self.server = Server(ui, repo, self.repowatcher, timeout)
670 self.table = {}
670 self.table = {}
671 for obj in (self.repowatcher, self.server):
671 for obj in (self.repowatcher, 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.repowatcher.setup()
684 self.repowatcher.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 def closefds(ignore):
712 def closefds(ignore):
713 # (from python bug #1177468)
713 # (from python bug #1177468)
714 # close all inherited file descriptors
714 # close all inherited file descriptors
715 # Python 2.4.1 and later use /dev/urandom to seed the random module's RNG
715 # Python 2.4.1 and later use /dev/urandom to seed the random module's RNG
716 # a file descriptor is kept internally as os._urandomfd (created on demand
716 # a file descriptor is kept internally as os._urandomfd (created on demand
717 # the first time os.urandom() is called), and should not be closed
717 # the first time os.urandom() is called), and should not be closed
718 try:
718 try:
719 os.urandom(4)
719 os.urandom(4)
720 urandom_fd = getattr(os, '_urandomfd', None)
720 urandom_fd = getattr(os, '_urandomfd', None)
721 except AttributeError:
721 except AttributeError:
722 urandom_fd = None
722 urandom_fd = None
723 ignore.append(urandom_fd)
723 ignore.append(urandom_fd)
724 for fd in range(3, 256):
724 for fd in range(3, 256):
725 if fd in ignore:
725 if fd in ignore:
726 continue
726 continue
727 try:
727 try:
728 os.close(fd)
728 os.close(fd)
729 except OSError:
729 except OSError:
730 pass
730 pass
731
731
732 m = Master(ui, repo)
732 m = Master(ui, repo)
733 sys.stdout.flush()
733 sys.stdout.flush()
734 sys.stderr.flush()
734 sys.stderr.flush()
735
735
736 pid = os.fork()
736 pid = os.fork()
737 if pid:
737 if pid:
738 return pid
738 return pid
739
739
740 closefds([m.server.fileno(), m.repowatcher.fileno()])
740 closefds([m.server.fileno(), m.repowatcher.fileno()])
741 os.setsid()
741 os.setsid()
742
742
743 fd = os.open('/dev/null', os.O_RDONLY)
743 fd = os.open('/dev/null', os.O_RDONLY)
744 os.dup2(fd, 0)
744 os.dup2(fd, 0)
745 if fd > 0:
745 if fd > 0:
746 os.close(fd)
746 os.close(fd)
747
747
748 fd = os.open(ui.config('inotify', 'log', '/dev/null'),
748 fd = os.open(ui.config('inotify', 'log', '/dev/null'),
749 os.O_RDWR | os.O_CREAT | os.O_TRUNC)
749 os.O_RDWR | os.O_CREAT | os.O_TRUNC)
750 os.dup2(fd, 1)
750 os.dup2(fd, 1)
751 os.dup2(fd, 2)
751 os.dup2(fd, 2)
752 if fd > 2:
752 if fd > 2:
753 os.close(fd)
753 os.close(fd)
754
754
755 try:
755 try:
756 m.run()
756 m.run()
757 finally:
757 finally:
758 m.shutdown()
758 m.shutdown()
759 os._exit(0)
759 os._exit(0)
General Comments 0
You need to be logged in to leave comments. Login now