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