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