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