##// END OF EJS Templates
inotify: completely ignore events on the repository root...
Nicolas Dumazet -
r10091:0ce645cc default
parent child Browse files
Show More
@@ -1,437 +1,441 b''
1 1 # linuxserver.py - inotify status server for linux
2 2 #
3 3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2, incorporated herein by reference.
8 8
9 9 from mercurial.i18n import _
10 10 from mercurial import osutil, util
11 11 import common
12 12 import server
13 13 import errno, os, select, stat, sys, time
14 14
15 15 try:
16 16 import linux as inotify
17 17 from linux import watcher
18 18 except ImportError:
19 19 raise
20 20
21 21 def walkrepodirs(dirstate, absroot):
22 22 '''Iterate over all subdirectories of this repo.
23 23 Exclude the .hg directory, any nested repos, and ignored dirs.'''
24 24 def walkit(dirname, top):
25 25 fullpath = server.join(absroot, dirname)
26 26 try:
27 27 for name, kind in osutil.listdir(fullpath):
28 28 if kind == stat.S_IFDIR:
29 29 if name == '.hg':
30 30 if not top:
31 31 return
32 32 else:
33 33 d = server.join(dirname, name)
34 34 if dirstate._ignore(d):
35 35 continue
36 36 for subdir in walkit(d, False):
37 37 yield subdir
38 38 except OSError, err:
39 39 if err.errno not in server.walk_ignored_errors:
40 40 raise
41 41 yield fullpath
42 42
43 43 return walkit('', True)
44 44
45 45 def _explain_watch_limit(ui, dirstate, rootabs):
46 46 path = '/proc/sys/fs/inotify/max_user_watches'
47 47 try:
48 48 limit = int(file(path).read())
49 49 except IOError, err:
50 50 if err.errno != errno.ENOENT:
51 51 raise
52 52 raise util.Abort(_('this system does not seem to '
53 53 'support inotify'))
54 54 ui.warn(_('*** the current per-user limit on the number '
55 55 'of inotify watches is %s\n') % limit)
56 56 ui.warn(_('*** this limit is too low to watch every '
57 57 'directory in this repository\n'))
58 58 ui.warn(_('*** counting directories: '))
59 59 ndirs = len(list(walkrepodirs(dirstate, rootabs)))
60 60 ui.warn(_('found %d\n') % ndirs)
61 61 newlimit = min(limit, 1024)
62 62 while newlimit < ((limit + ndirs) * 1.1):
63 63 newlimit *= 2
64 64 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
65 65 (limit, newlimit))
66 66 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
67 67 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
68 68 % rootabs)
69 69
70 70 class pollable(object):
71 71 """
72 72 Interface to support polling.
73 73 The file descriptor returned by fileno() is registered to a polling
74 74 object.
75 75 Usage:
76 76 Every tick, check if an event has happened since the last tick:
77 77 * If yes, call handle_events
78 78 * If no, call handle_timeout
79 79 """
80 80 poll_events = select.POLLIN
81 81 instances = {}
82 82 poll = select.poll()
83 83
84 84 def fileno(self):
85 85 raise NotImplementedError
86 86
87 87 def handle_events(self, events):
88 88 raise NotImplementedError
89 89
90 90 def handle_timeout(self):
91 91 raise NotImplementedError
92 92
93 93 def shutdown(self):
94 94 raise NotImplementedError
95 95
96 96 def register(self, timeout):
97 97 fd = self.fileno()
98 98
99 99 pollable.poll.register(fd, pollable.poll_events)
100 100 pollable.instances[fd] = self
101 101
102 102 self.registered = True
103 103 self.timeout = timeout
104 104
105 105 def unregister(self):
106 106 pollable.poll.unregister(self)
107 107 self.registered = False
108 108
109 109 @classmethod
110 110 def run(cls):
111 111 while True:
112 112 timeout = None
113 113 timeobj = None
114 114 for obj in cls.instances.itervalues():
115 115 if obj.timeout is not None and (timeout is None or obj.timeout < timeout):
116 116 timeout, timeobj = obj.timeout, obj
117 117 try:
118 118 events = cls.poll.poll(timeout)
119 119 except select.error, err:
120 120 if err[0] == errno.EINTR:
121 121 continue
122 122 raise
123 123 if events:
124 124 by_fd = {}
125 125 for fd, event in events:
126 126 by_fd.setdefault(fd, []).append(event)
127 127
128 128 for fd, events in by_fd.iteritems():
129 129 cls.instances[fd].handle_pollevents(events)
130 130
131 131 elif timeobj:
132 132 timeobj.handle_timeout()
133 133
134 134 def eventaction(code):
135 135 """
136 136 Decorator to help handle events in repowatcher
137 137 """
138 138 def decorator(f):
139 139 def wrapper(self, wpath):
140 140 if code == 'm' and wpath in self.lastevent and \
141 141 self.lastevent[wpath] in 'cm':
142 142 return
143 143 self.lastevent[wpath] = code
144 144 self.timeout = 250
145 145
146 146 f(self, wpath)
147 147
148 148 wrapper.func_name = f.func_name
149 149 return wrapper
150 150 return decorator
151 151
152 152 class repowatcher(server.repowatcher, pollable):
153 153 """
154 154 Watches inotify events
155 155 """
156 156 mask = (
157 157 inotify.IN_ATTRIB |
158 158 inotify.IN_CREATE |
159 159 inotify.IN_DELETE |
160 160 inotify.IN_DELETE_SELF |
161 161 inotify.IN_MODIFY |
162 162 inotify.IN_MOVED_FROM |
163 163 inotify.IN_MOVED_TO |
164 164 inotify.IN_MOVE_SELF |
165 165 inotify.IN_ONLYDIR |
166 166 inotify.IN_UNMOUNT |
167 167 0)
168 168
169 169 def __init__(self, ui, dirstate, root):
170 170 server.repowatcher.__init__(self, ui, dirstate, root)
171 171
172 172 self.lastevent = {}
173 173 self.dirty = False
174 174 try:
175 175 self.watcher = watcher.watcher()
176 176 except OSError, err:
177 177 raise util.Abort(_('inotify service not available: %s') %
178 178 err.strerror)
179 179 self.threshold = watcher.threshold(self.watcher)
180 180 self.fileno = self.watcher.fileno
181 181 self.register(timeout=None)
182 182
183 183 self.handle_timeout()
184 184 self.scan()
185 185
186 186 def event_time(self):
187 187 last = self.last_event
188 188 now = time.time()
189 189 self.last_event = now
190 190
191 191 if last is None:
192 192 return 'start'
193 193 delta = now - last
194 194 if delta < 5:
195 195 return '+%.3f' % delta
196 196 if delta < 50:
197 197 return '+%.2f' % delta
198 198 return '+%.1f' % delta
199 199
200 200 def add_watch(self, path, mask):
201 201 if not path:
202 202 return
203 203 if self.watcher.path(path) is None:
204 204 if self.ui.debugflag:
205 205 self.ui.note(_('watching %r\n') % path[self.prefixlen:])
206 206 try:
207 207 self.watcher.add(path, mask)
208 208 except OSError, err:
209 209 if err.errno in (errno.ENOENT, errno.ENOTDIR):
210 210 return
211 211 if err.errno != errno.ENOSPC:
212 212 raise
213 213 _explain_watch_limit(self.ui, self.dirstate, self.wprefix)
214 214
215 215 def setup(self):
216 216 self.ui.note(_('watching directories under %r\n') % self.wprefix)
217 217 self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE)
218 218
219 219 def scan(self, topdir=''):
220 220 ds = self.dirstate._map.copy()
221 221 self.add_watch(server.join(self.wprefix, topdir), self.mask)
222 222 for root, dirs, files in server.walk(self.dirstate, self.wprefix,
223 223 topdir):
224 224 for d in dirs:
225 225 self.add_watch(server.join(root, d), self.mask)
226 226 wroot = root[self.prefixlen:]
227 227 for fn in files:
228 228 wfn = server.join(wroot, fn)
229 229 self.updatefile(wfn, self.getstat(wfn))
230 230 ds.pop(wfn, None)
231 231 wtopdir = topdir
232 232 if wtopdir and wtopdir[-1] != '/':
233 233 wtopdir += '/'
234 234 for wfn, state in ds.iteritems():
235 235 if not wfn.startswith(wtopdir):
236 236 continue
237 237 try:
238 238 st = self.stat(wfn)
239 239 except OSError:
240 240 status = state[0]
241 241 self.deletefile(wfn, status)
242 242 else:
243 243 self.updatefile(wfn, st)
244 244 self.check_deleted('!')
245 245 self.check_deleted('r')
246 246
247 247 @eventaction('c')
248 248 def created(self, wpath):
249 249 if wpath == '.hgignore':
250 250 self.update_hgignore()
251 251 try:
252 252 st = self.stat(wpath)
253 253 if stat.S_ISREG(st[0]) or stat.S_ISLNK(st[0]):
254 254 self.updatefile(wpath, st)
255 255 except OSError:
256 256 pass
257 257
258 258 @eventaction('m')
259 259 def modified(self, wpath):
260 260 if wpath == '.hgignore':
261 261 self.update_hgignore()
262 262 try:
263 263 st = self.stat(wpath)
264 264 if stat.S_ISREG(st[0]):
265 265 if self.dirstate[wpath] in 'lmn':
266 266 self.updatefile(wpath, st)
267 267 except OSError:
268 268 pass
269 269
270 270 @eventaction('d')
271 271 def deleted(self, wpath):
272 272 if wpath == '.hgignore':
273 273 self.update_hgignore()
274 274 elif wpath.startswith('.hg/'):
275 275 return
276 276
277 277 self.deletefile(wpath, self.dirstate[wpath])
278 278
279 279 def process_create(self, wpath, evt):
280 280 if self.ui.debugflag:
281 281 self.ui.note(_('%s event: created %s\n') %
282 282 (self.event_time(), wpath))
283 283
284 284 if evt.mask & inotify.IN_ISDIR:
285 285 self.scan(wpath)
286 286 else:
287 287 self.created(wpath)
288 288
289 289 def process_delete(self, wpath, evt):
290 290 if self.ui.debugflag:
291 291 self.ui.note(_('%s event: deleted %s\n') %
292 292 (self.event_time(), wpath))
293 293
294 294 if evt.mask & inotify.IN_ISDIR:
295 295 tree = self.tree.dir(wpath)
296 296 todelete = [wfn for wfn, ignore in tree.walk('?')]
297 297 for fn in todelete:
298 298 self.deletefile(fn, '?')
299 299 self.scan(wpath)
300 300 else:
301 301 self.deleted(wpath)
302 302
303 303 def process_modify(self, wpath, evt):
304 304 if self.ui.debugflag:
305 305 self.ui.note(_('%s event: modified %s\n') %
306 306 (self.event_time(), wpath))
307 307
308 308 if not (evt.mask & inotify.IN_ISDIR):
309 309 self.modified(wpath)
310 310
311 311 def process_unmount(self, evt):
312 312 self.ui.warn(_('filesystem containing %s was unmounted\n') %
313 313 evt.fullpath)
314 314 sys.exit(0)
315 315
316 316 def handle_pollevents(self, events):
317 317 if self.ui.debugflag:
318 318 self.ui.note(_('%s readable: %d bytes\n') %
319 319 (self.event_time(), self.threshold.readable()))
320 320 if not self.threshold():
321 321 if self.registered:
322 322 if self.ui.debugflag:
323 323 self.ui.note(_('%s below threshold - unhooking\n') %
324 324 (self.event_time()))
325 325 self.unregister()
326 326 self.timeout = 250
327 327 else:
328 328 self.read_events()
329 329
330 330 def read_events(self, bufsize=None):
331 331 events = self.watcher.read(bufsize)
332 332 if self.ui.debugflag:
333 333 self.ui.note(_('%s reading %d events\n') %
334 334 (self.event_time(), len(events)))
335 335 for evt in events:
336 if evt.fullpath == self.wprefix[:-1]:
337 # events on the root of the repository
338 # itself, e.g. permission changes or repository move
339 continue
336 340 assert evt.fullpath.startswith(self.wprefix)
337 341 wpath = evt.fullpath[self.prefixlen:]
338 342
339 343 # paths have been normalized, wpath never ends with a '/'
340 344
341 345 if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR:
342 346 # ignore subdirectories of .hg/ (merge, patches...)
343 347 continue
344 348 if wpath == ".hg/wlock":
345 349 if evt.mask & inotify.IN_DELETE:
346 350 self.dirstate.invalidate()
347 351 self.dirty = False
348 352 self.scan()
349 353 elif evt.mask & inotify.IN_CREATE:
350 354 self.dirty = True
351 355 else:
352 356 if self.dirty:
353 357 continue
354 358
355 359 if evt.mask & inotify.IN_UNMOUNT:
356 360 self.process_unmount(wpath, evt)
357 361 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
358 362 self.process_modify(wpath, evt)
359 363 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
360 364 inotify.IN_MOVED_FROM):
361 365 self.process_delete(wpath, evt)
362 366 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
363 367 self.process_create(wpath, evt)
364 368
365 369 self.lastevent.clear()
366 370
367 371 def handle_timeout(self):
368 372 if not self.registered:
369 373 if self.ui.debugflag:
370 374 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
371 375 (self.event_time(), self.threshold.readable()))
372 376 self.read_events(0)
373 377 self.register(timeout=None)
374 378
375 379 self.timeout = None
376 380
377 381 def shutdown(self):
378 382 self.watcher.close()
379 383
380 384 def debug(self):
381 385 """
382 386 Returns a sorted list of relatives paths currently watched,
383 387 for debugging purposes.
384 388 """
385 389 return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher)
386 390
387 391 class socketlistener(server.socketlistener, pollable):
388 392 """
389 393 Listens for client queries on unix socket inotify.sock
390 394 """
391 395 def __init__(self, ui, root, repowatcher, timeout):
392 396 server.socketlistener.__init__(self, ui, root, repowatcher, timeout)
393 397 self.register(timeout=timeout)
394 398
395 399 def handle_timeout(self):
396 400 pass
397 401
398 402 def handle_pollevents(self, events):
399 403 for e in events:
400 404 self.accept_connection()
401 405
402 406 def shutdown(self):
403 407 self.sock.close()
404 408 try:
405 409 os.unlink(self.sockpath)
406 410 if self.realsockpath:
407 411 os.unlink(self.realsockpath)
408 412 os.rmdir(os.path.dirname(self.realsockpath))
409 413 except OSError, err:
410 414 if err.errno != errno.ENOENT:
411 415 raise
412 416
413 417 def answer_stat_query(self, cs):
414 418 if self.repowatcher.timeout:
415 419 # We got a query while a rescan is pending. Make sure we
416 420 # rescan before responding, or we could give back a wrong
417 421 # answer.
418 422 self.repowatcher.handle_timeout()
419 423 return server.socketlistener.answer_stat_query(self, cs)
420 424
421 425 class master(object):
422 426 def __init__(self, ui, dirstate, root, timeout=None):
423 427 self.ui = ui
424 428 self.repowatcher = repowatcher(ui, dirstate, root)
425 429 self.socketlistener = socketlistener(ui, root, self.repowatcher,
426 430 timeout)
427 431
428 432 def shutdown(self):
429 433 for obj in pollable.instances.itervalues():
430 434 obj.shutdown()
431 435
432 436 def run(self):
433 437 self.repowatcher.setup()
434 438 self.ui.note(_('finished setup\n'))
435 439 if os.getenv('TIME_STARTUP'):
436 440 sys.exit(0)
437 441 pollable.run()
General Comments 0
You need to be logged in to leave comments. Login now