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