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