##// END OF EJS Templates
inotify: improve error messages...
Nicolas Dumazet -
r9900:89399000 stable
parent child Browse files
Show More
@@ -1,170 +1,171
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(_('inotify-client: found dead inotify server '
31 'removing it)\n'))
31 'socket; 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 dict(daemon=True, daemon_pipefds=''))
39 except server.AlreadyStartedException, inst:
39 except server.AlreadyStartedException, inst:
40 # another process may have started its own
40 # another process may have started its own
41 # inotify server while this one was starting.
41 # inotify server while this one was starting.
42 self.ui.debug(str(inst))
42 self.ui.debug(str(inst))
43 except Exception, inst:
43 except Exception, inst:
44 self.ui.warn(_('could not start inotify server: '
44 self.ui.warn(_('inotify-client: could not start inotify '
45 '%s\n') % inst)
45 'server: %s\n') % inst)
46 else:
46 else:
47 try:
47 try:
48 return function(self, *args)
48 return function(self, *args)
49 except socket.error, err:
49 except socket.error, err:
50 self.ui.warn(_('could not talk to new inotify '
50 self.ui.warn(_('inotify-client: could not talk to new '
51 'server: %s\n') % err[-1])
51 'inotify server: %s\n') % err[-1])
52 elif err[0] in (errno.ECONNREFUSED, errno.ENOENT):
52 elif err[0] in (errno.ECONNREFUSED, errno.ENOENT):
53 # silently ignore normal errors if autostart is False
53 # silently ignore normal errors if autostart is False
54 self.ui.debug('(inotify server not running)\n')
54 self.ui.debug('(inotify server not running)\n')
55 else:
55 else:
56 self.ui.warn(_('failed to contact inotify server: %s\n')
56 self.ui.warn(_('inotify-client: failed to contact inotify '
57 % err[-1])
57 'server: %s\n') % err[-1])
58
58
59 self.ui.traceback()
59 self.ui.traceback()
60 raise QueryFailed('inotify query failed')
60 raise QueryFailed('inotify query failed')
61
61
62 return decorated_function
62 return decorated_function
63
63
64
64
65 class client(object):
65 class client(object):
66 def __init__(self, ui, repo):
66 def __init__(self, ui, repo):
67 self.ui = ui
67 self.ui = ui
68 self.dirstate = repo.dirstate
68 self.dirstate = repo.dirstate
69 self.root = repo.root
69 self.root = repo.root
70 self.sock = socket.socket(socket.AF_UNIX)
70 self.sock = socket.socket(socket.AF_UNIX)
71
71
72 def _connect(self):
72 def _connect(self):
73 sockpath = os.path.join(self.root, '.hg', 'inotify.sock')
73 sockpath = os.path.join(self.root, '.hg', 'inotify.sock')
74 try:
74 try:
75 self.sock.connect(sockpath)
75 self.sock.connect(sockpath)
76 except socket.error, err:
76 except socket.error, err:
77 if err[0] == "AF_UNIX path too long":
77 if err[0] == "AF_UNIX path too long":
78 sockpath = os.readlink(sockpath)
78 sockpath = os.readlink(sockpath)
79 self.sock.connect(sockpath)
79 self.sock.connect(sockpath)
80 else:
80 else:
81 raise
81 raise
82
82
83 def _send(self, type, data):
83 def _send(self, type, data):
84 """Sends protocol version number, and the data"""
84 """Sends protocol version number, and the data"""
85 self.sock.sendall(chr(common.version) + type + data)
85 self.sock.sendall(chr(common.version) + type + data)
86
86
87 self.sock.shutdown(socket.SHUT_WR)
87 self.sock.shutdown(socket.SHUT_WR)
88
88
89 def _receive(self, type):
89 def _receive(self, type):
90 """
90 """
91 Read data, check version number, extract headers,
91 Read data, check version number, extract headers,
92 and returns a tuple (data descriptor, header)
92 and returns a tuple (data descriptor, header)
93 Raises QueryFailed on error
93 Raises QueryFailed on error
94 """
94 """
95 cs = common.recvcs(self.sock)
95 cs = common.recvcs(self.sock)
96 try:
96 try:
97 version = ord(cs.read(1))
97 version = ord(cs.read(1))
98 except TypeError:
98 except TypeError:
99 # empty answer, assume the server crashed
99 # empty answer, assume the server crashed
100 self.ui.warn(_('received empty answer from inotify server'))
100 self.ui.warn(_('inotify-client: received empty answer from inotify '
101 'server'))
101 raise QueryFailed('server crashed')
102 raise QueryFailed('server crashed')
102
103
103 if version != common.version:
104 if version != common.version:
104 self.ui.warn(_('(inotify: received response from incompatible '
105 self.ui.warn(_('(inotify: received response from incompatible '
105 'server version %d)\n') % version)
106 'server version %d)\n') % version)
106 raise QueryFailed('incompatible server version')
107 raise QueryFailed('incompatible server version')
107
108
108 readtype = cs.read(4)
109 readtype = cs.read(4)
109 if readtype != type:
110 if readtype != type:
110 self.ui.warn(_('(inotify: received \'%s\' response when expecting'
111 self.ui.warn(_('(inotify: received \'%s\' response when expecting'
111 ' \'%s\')\n') % (readtype, type))
112 ' \'%s\')\n') % (readtype, type))
112 raise QueryFailed('wrong response type')
113 raise QueryFailed('wrong response type')
113
114
114 hdrfmt = common.resphdrfmts[type]
115 hdrfmt = common.resphdrfmts[type]
115 hdrsize = common.resphdrsizes[type]
116 hdrsize = common.resphdrsizes[type]
116 try:
117 try:
117 resphdr = struct.unpack(hdrfmt, cs.read(hdrsize))
118 resphdr = struct.unpack(hdrfmt, cs.read(hdrsize))
118 except struct.error:
119 except struct.error:
119 raise QueryFailed('unable to retrieve query response headers')
120 raise QueryFailed('unable to retrieve query response headers')
120
121
121 return cs, resphdr
122 return cs, resphdr
122
123
123 def query(self, type, req):
124 def query(self, type, req):
124 self._connect()
125 self._connect()
125
126
126 self._send(type, req)
127 self._send(type, req)
127
128
128 return self._receive(type)
129 return self._receive(type)
129
130
130 @start_server
131 @start_server
131 def statusquery(self, names, match, ignored, clean, unknown=True):
132 def statusquery(self, names, match, ignored, clean, unknown=True):
132
133
133 def genquery():
134 def genquery():
134 for n in names:
135 for n in names:
135 yield n
136 yield n
136 states = 'almrx!'
137 states = 'almrx!'
137 if ignored:
138 if ignored:
138 raise ValueError('this is insanity')
139 raise ValueError('this is insanity')
139 if clean: states += 'c'
140 if clean: states += 'c'
140 if unknown: states += '?'
141 if unknown: states += '?'
141 yield states
142 yield states
142
143
143 req = '\0'.join(genquery())
144 req = '\0'.join(genquery())
144
145
145 cs, resphdr = self.query('STAT', req)
146 cs, resphdr = self.query('STAT', req)
146
147
147 def readnames(nbytes):
148 def readnames(nbytes):
148 if nbytes:
149 if nbytes:
149 names = cs.read(nbytes)
150 names = cs.read(nbytes)
150 if names:
151 if names:
151 return filter(match, names.split('\0'))
152 return filter(match, names.split('\0'))
152 return []
153 return []
153 results = map(readnames, resphdr[:-1])
154 results = map(readnames, resphdr[:-1])
154
155
155 if names:
156 if names:
156 nbytes = resphdr[-1]
157 nbytes = resphdr[-1]
157 vdirs = cs.read(nbytes)
158 vdirs = cs.read(nbytes)
158 if vdirs:
159 if vdirs:
159 for vdir in vdirs.split('\0'):
160 for vdir in vdirs.split('\0'):
160 match.dir(vdir)
161 match.dir(vdir)
161
162
162 return results
163 return results
163
164
164 @start_server
165 @start_server
165 def debugquery(self):
166 def debugquery(self):
166 cs, resphdr = self.query('DBUG', '')
167 cs, resphdr = self.query('DBUG', '')
167
168
168 nbytes = resphdr[0]
169 nbytes = resphdr[0]
169 names = cs.read(nbytes)
170 names = cs.read(nbytes)
170 return names.split('\0')
171 return names.split('\0')
@@ -1,864 +1,869
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 cmdutil, 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, visited=None):
240 def walk(self, states, visited=None):
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 if visited is not None:
250 if visited is not None:
251 visited.add(dir.path)
251 visited.add(dir.path)
252 for e in dir.walk(states):
252 for e in dir.walk(states):
253 yield e
253 yield e
254
254
255 def lookup(self, states, path, visited):
255 def lookup(self, states, path, visited):
256 """
256 """
257 yield root-relative filenames that match path, and whose
257 yield root-relative filenames that match path, and whose
258 status are in states:
258 status are in states:
259 * if path is a file, yield path
259 * if path is a file, yield path
260 * if path is a directory, yield directory files
260 * if path is a directory, yield directory files
261 * if path is not tracked, yield nothing
261 * if path is not tracked, yield nothing
262 """
262 """
263 if path[-1] == '/':
263 if path[-1] == '/':
264 path = path[:-1]
264 path = path[:-1]
265
265
266 paths = path.split('/')
266 paths = path.split('/')
267
267
268 # we need to check separately for last node
268 # we need to check separately for last node
269 last = paths.pop()
269 last = paths.pop()
270
270
271 tree = self
271 tree = self
272 try:
272 try:
273 for dir in paths:
273 for dir in paths:
274 tree = tree.dirs[dir]
274 tree = tree.dirs[dir]
275 except KeyError:
275 except KeyError:
276 # path is not tracked
276 # path is not tracked
277 visited.add(tree.path)
277 visited.add(tree.path)
278 return
278 return
279
279
280 try:
280 try:
281 # if path is a directory, walk it
281 # if path is a directory, walk it
282 target = tree.dirs[last]
282 target = tree.dirs[last]
283 visited.add(target.path)
283 visited.add(target.path)
284 for file, st in target.walk(states, visited):
284 for file, st in target.walk(states, visited):
285 yield file
285 yield file
286 except KeyError:
286 except KeyError:
287 try:
287 try:
288 if tree.files[last] in states:
288 if tree.files[last] in states:
289 # path is a file
289 # path is a file
290 visited.add(tree.path)
290 visited.add(tree.path)
291 yield path
291 yield path
292 except KeyError:
292 except KeyError:
293 # path is not tracked
293 # path is not tracked
294 pass
294 pass
295
295
296 class repowatcher(pollable):
296 class repowatcher(pollable):
297 """
297 """
298 Watches inotify events
298 Watches inotify events
299 """
299 """
300 statuskeys = 'almr!?'
300 statuskeys = 'almr!?'
301 mask = (
301 mask = (
302 inotify.IN_ATTRIB |
302 inotify.IN_ATTRIB |
303 inotify.IN_CREATE |
303 inotify.IN_CREATE |
304 inotify.IN_DELETE |
304 inotify.IN_DELETE |
305 inotify.IN_DELETE_SELF |
305 inotify.IN_DELETE_SELF |
306 inotify.IN_MODIFY |
306 inotify.IN_MODIFY |
307 inotify.IN_MOVED_FROM |
307 inotify.IN_MOVED_FROM |
308 inotify.IN_MOVED_TO |
308 inotify.IN_MOVED_TO |
309 inotify.IN_MOVE_SELF |
309 inotify.IN_MOVE_SELF |
310 inotify.IN_ONLYDIR |
310 inotify.IN_ONLYDIR |
311 inotify.IN_UNMOUNT |
311 inotify.IN_UNMOUNT |
312 0)
312 0)
313
313
314 def __init__(self, ui, dirstate, root):
314 def __init__(self, ui, dirstate, root):
315 self.ui = ui
315 self.ui = ui
316 self.dirstate = dirstate
316 self.dirstate = dirstate
317
317
318 self.wprefix = join(root, '')
318 self.wprefix = join(root, '')
319 self.prefixlen = len(self.wprefix)
319 self.prefixlen = len(self.wprefix)
320 try:
320 try:
321 self.watcher = watcher.watcher()
321 self.watcher = watcher.watcher()
322 except OSError, err:
322 except OSError, err:
323 raise util.Abort(_('inotify service not available: %s') %
323 raise util.Abort(_('inotify service not available: %s') %
324 err.strerror)
324 err.strerror)
325 self.threshold = watcher.threshold(self.watcher)
325 self.threshold = watcher.threshold(self.watcher)
326 self.fileno = self.watcher.fileno
326 self.fileno = self.watcher.fileno
327
327
328 self.tree = directory()
328 self.tree = directory()
329 self.statcache = {}
329 self.statcache = {}
330 self.statustrees = dict([(s, directory()) for s in self.statuskeys])
330 self.statustrees = dict([(s, directory()) for s in self.statuskeys])
331
331
332 self.last_event = None
332 self.last_event = None
333
333
334 self.lastevent = {}
334 self.lastevent = {}
335
335
336 self.register(timeout=None)
336 self.register(timeout=None)
337
337
338 self.ds_info = self.dirstate_info()
338 self.ds_info = self.dirstate_info()
339 self.handle_timeout()
339 self.handle_timeout()
340 self.scan()
340 self.scan()
341
341
342 def event_time(self):
342 def event_time(self):
343 last = self.last_event
343 last = self.last_event
344 now = time.time()
344 now = time.time()
345 self.last_event = now
345 self.last_event = now
346
346
347 if last is None:
347 if last is None:
348 return 'start'
348 return 'start'
349 delta = now - last
349 delta = now - last
350 if delta < 5:
350 if delta < 5:
351 return '+%.3f' % delta
351 return '+%.3f' % delta
352 if delta < 50:
352 if delta < 50:
353 return '+%.2f' % delta
353 return '+%.2f' % delta
354 return '+%.1f' % delta
354 return '+%.1f' % delta
355
355
356 def dirstate_info(self):
356 def dirstate_info(self):
357 try:
357 try:
358 st = os.lstat(self.wprefix + '.hg/dirstate')
358 st = os.lstat(self.wprefix + '.hg/dirstate')
359 return st.st_mtime, st.st_ino
359 return st.st_mtime, st.st_ino
360 except OSError, err:
360 except OSError, err:
361 if err.errno != errno.ENOENT:
361 if err.errno != errno.ENOENT:
362 raise
362 raise
363 return 0, 0
363 return 0, 0
364
364
365 def add_watch(self, path, mask):
365 def add_watch(self, path, mask):
366 if not path:
366 if not path:
367 return
367 return
368 if self.watcher.path(path) is None:
368 if self.watcher.path(path) is None:
369 if self.ui.debugflag:
369 if self.ui.debugflag:
370 self.ui.note(_('watching %r\n') % path[self.prefixlen:])
370 self.ui.note(_('watching %r\n') % path[self.prefixlen:])
371 try:
371 try:
372 self.watcher.add(path, mask)
372 self.watcher.add(path, mask)
373 except OSError, err:
373 except OSError, err:
374 if err.errno in (errno.ENOENT, errno.ENOTDIR):
374 if err.errno in (errno.ENOENT, errno.ENOTDIR):
375 return
375 return
376 if err.errno != errno.ENOSPC:
376 if err.errno != errno.ENOSPC:
377 raise
377 raise
378 _explain_watch_limit(self.ui, self.dirstate, self.wprefix)
378 _explain_watch_limit(self.ui, self.dirstate, self.wprefix)
379
379
380 def setup(self):
380 def setup(self):
381 self.ui.note(_('watching directories under %r\n') % self.wprefix)
381 self.ui.note(_('watching directories under %r\n') % self.wprefix)
382 self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE)
382 self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE)
383 self.check_dirstate()
383 self.check_dirstate()
384
384
385 def filestatus(self, fn, st):
385 def filestatus(self, fn, st):
386 try:
386 try:
387 type_, mode, size, time = self.dirstate._map[fn][:4]
387 type_, mode, size, time = self.dirstate._map[fn][:4]
388 except KeyError:
388 except KeyError:
389 type_ = '?'
389 type_ = '?'
390 if type_ == 'n':
390 if type_ == 'n':
391 st_mode, st_size, st_mtime = st
391 st_mode, st_size, st_mtime = st
392 if size == -1:
392 if size == -1:
393 return 'l'
393 return 'l'
394 if size and (size != st_size or (mode ^ st_mode) & 0100):
394 if size and (size != st_size or (mode ^ st_mode) & 0100):
395 return 'm'
395 return 'm'
396 if time != int(st_mtime):
396 if time != int(st_mtime):
397 return 'l'
397 return 'l'
398 return 'n'
398 return 'n'
399 if type_ == '?' and self.dirstate._ignore(fn):
399 if type_ == '?' and self.dirstate._ignore(fn):
400 return 'i'
400 return 'i'
401 return type_
401 return type_
402
402
403 def updatefile(self, wfn, osstat):
403 def updatefile(self, wfn, osstat):
404 '''
404 '''
405 update the file entry of an existing file.
405 update the file entry of an existing file.
406
406
407 osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
407 osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
408 '''
408 '''
409
409
410 self._updatestatus(wfn, self.filestatus(wfn, osstat))
410 self._updatestatus(wfn, self.filestatus(wfn, osstat))
411
411
412 def deletefile(self, wfn, oldstatus):
412 def deletefile(self, wfn, oldstatus):
413 '''
413 '''
414 update the entry of a file which has been deleted.
414 update the entry of a file which has been deleted.
415
415
416 oldstatus: char in statuskeys, status of the file before deletion
416 oldstatus: char in statuskeys, status of the file before deletion
417 '''
417 '''
418 if oldstatus == 'r':
418 if oldstatus == 'r':
419 newstatus = 'r'
419 newstatus = 'r'
420 elif oldstatus in 'almn':
420 elif oldstatus in 'almn':
421 newstatus = '!'
421 newstatus = '!'
422 else:
422 else:
423 newstatus = None
423 newstatus = None
424
424
425 self.statcache.pop(wfn, None)
425 self.statcache.pop(wfn, None)
426 self._updatestatus(wfn, newstatus)
426 self._updatestatus(wfn, newstatus)
427
427
428 def _updatestatus(self, wfn, newstatus):
428 def _updatestatus(self, wfn, newstatus):
429 '''
429 '''
430 Update the stored status of a file.
430 Update the stored status of a file.
431
431
432 newstatus: - char in (statuskeys + 'ni'), new status to apply.
432 newstatus: - char in (statuskeys + 'ni'), new status to apply.
433 - or None, to stop tracking wfn
433 - or None, to stop tracking wfn
434 '''
434 '''
435 root, fn = split(wfn)
435 root, fn = split(wfn)
436 d = self.tree.dir(root)
436 d = self.tree.dir(root)
437
437
438 oldstatus = d.files.get(fn)
438 oldstatus = d.files.get(fn)
439 # oldstatus can be either:
439 # oldstatus can be either:
440 # - None : fn is new
440 # - None : fn is new
441 # - a char in statuskeys: fn is a (tracked) file
441 # - a char in statuskeys: fn is a (tracked) file
442
442
443 if self.ui.debugflag and oldstatus != newstatus:
443 if self.ui.debugflag and oldstatus != newstatus:
444 self.ui.note(_('status: %r %s -> %s\n') %
444 self.ui.note(_('status: %r %s -> %s\n') %
445 (wfn, oldstatus, newstatus))
445 (wfn, oldstatus, newstatus))
446
446
447 if oldstatus and oldstatus in self.statuskeys \
447 if oldstatus and oldstatus in self.statuskeys \
448 and oldstatus != newstatus:
448 and oldstatus != newstatus:
449 del self.statustrees[oldstatus].dir(root).files[fn]
449 del self.statustrees[oldstatus].dir(root).files[fn]
450
450
451 if newstatus in (None, 'i'):
451 if newstatus in (None, 'i'):
452 d.files.pop(fn, None)
452 d.files.pop(fn, None)
453 elif oldstatus != newstatus:
453 elif oldstatus != newstatus:
454 d.files[fn] = newstatus
454 d.files[fn] = newstatus
455 if newstatus != 'n':
455 if newstatus != 'n':
456 self.statustrees[newstatus].dir(root).files[fn] = newstatus
456 self.statustrees[newstatus].dir(root).files[fn] = newstatus
457
457
458
458
459 def check_deleted(self, key):
459 def check_deleted(self, key):
460 # Files that had been deleted but were present in the dirstate
460 # Files that had been deleted but were present in the dirstate
461 # may have vanished from the dirstate; we must clean them up.
461 # may have vanished from the dirstate; we must clean them up.
462 nuke = []
462 nuke = []
463 for wfn, ignore in self.statustrees[key].walk(key):
463 for wfn, ignore in self.statustrees[key].walk(key):
464 if wfn not in self.dirstate:
464 if wfn not in self.dirstate:
465 nuke.append(wfn)
465 nuke.append(wfn)
466 for wfn in nuke:
466 for wfn in nuke:
467 root, fn = split(wfn)
467 root, fn = split(wfn)
468 del self.statustrees[key].dir(root).files[fn]
468 del self.statustrees[key].dir(root).files[fn]
469 del self.tree.dir(root).files[fn]
469 del self.tree.dir(root).files[fn]
470
470
471 def scan(self, topdir=''):
471 def scan(self, topdir=''):
472 ds = self.dirstate._map.copy()
472 ds = self.dirstate._map.copy()
473 self.add_watch(join(self.wprefix, topdir), self.mask)
473 self.add_watch(join(self.wprefix, topdir), self.mask)
474 for root, dirs, files in walk(self.dirstate, self.wprefix, topdir):
474 for root, dirs, files in walk(self.dirstate, self.wprefix, topdir):
475 for d in dirs:
475 for d in dirs:
476 self.add_watch(join(root, d), self.mask)
476 self.add_watch(join(root, d), self.mask)
477 wroot = root[self.prefixlen:]
477 wroot = root[self.prefixlen:]
478 for fn in files:
478 for fn in files:
479 wfn = join(wroot, fn)
479 wfn = join(wroot, fn)
480 self.updatefile(wfn, self.getstat(wfn))
480 self.updatefile(wfn, self.getstat(wfn))
481 ds.pop(wfn, None)
481 ds.pop(wfn, None)
482 wtopdir = topdir
482 wtopdir = topdir
483 if wtopdir and wtopdir[-1] != '/':
483 if wtopdir and wtopdir[-1] != '/':
484 wtopdir += '/'
484 wtopdir += '/'
485 for wfn, state in ds.iteritems():
485 for wfn, state in ds.iteritems():
486 if not wfn.startswith(wtopdir):
486 if not wfn.startswith(wtopdir):
487 continue
487 continue
488 try:
488 try:
489 st = self.stat(wfn)
489 st = self.stat(wfn)
490 except OSError:
490 except OSError:
491 status = state[0]
491 status = state[0]
492 self.deletefile(wfn, status)
492 self.deletefile(wfn, status)
493 else:
493 else:
494 self.updatefile(wfn, st)
494 self.updatefile(wfn, st)
495 self.check_deleted('!')
495 self.check_deleted('!')
496 self.check_deleted('r')
496 self.check_deleted('r')
497
497
498 def check_dirstate(self):
498 def check_dirstate(self):
499 ds_info = self.dirstate_info()
499 ds_info = self.dirstate_info()
500 if ds_info == self.ds_info:
500 if ds_info == self.ds_info:
501 return
501 return
502 self.ds_info = ds_info
502 self.ds_info = ds_info
503 if not self.ui.debugflag:
503 if not self.ui.debugflag:
504 self.last_event = None
504 self.last_event = None
505 self.ui.note(_('%s dirstate reload\n') % self.event_time())
505 self.ui.note(_('%s dirstate reload\n') % self.event_time())
506 self.dirstate.invalidate()
506 self.dirstate.invalidate()
507 self.handle_timeout()
507 self.handle_timeout()
508 self.scan()
508 self.scan()
509 self.ui.note(_('%s end dirstate reload\n') % self.event_time())
509 self.ui.note(_('%s end dirstate reload\n') % self.event_time())
510
510
511 def update_hgignore(self):
511 def update_hgignore(self):
512 # An update of the ignore file can potentially change the
512 # An update of the ignore file can potentially change the
513 # states of all unknown and ignored files.
513 # states of all unknown and ignored files.
514
514
515 # XXX If the user has other ignore files outside the repo, or
515 # XXX If the user has other ignore files outside the repo, or
516 # changes their list of ignore files at run time, we'll
516 # changes their list of ignore files at run time, we'll
517 # potentially never see changes to them. We could get the
517 # potentially never see changes to them. We could get the
518 # client to report to us what ignore data they're using.
518 # client to report to us what ignore data they're using.
519 # But it's easier to do nothing than to open that can of
519 # But it's easier to do nothing than to open that can of
520 # worms.
520 # worms.
521
521
522 if '_ignore' in self.dirstate.__dict__:
522 if '_ignore' in self.dirstate.__dict__:
523 delattr(self.dirstate, '_ignore')
523 delattr(self.dirstate, '_ignore')
524 self.ui.note(_('rescanning due to .hgignore change\n'))
524 self.ui.note(_('rescanning due to .hgignore change\n'))
525 self.handle_timeout()
525 self.handle_timeout()
526 self.scan()
526 self.scan()
527
527
528 def getstat(self, wpath):
528 def getstat(self, wpath):
529 try:
529 try:
530 return self.statcache[wpath]
530 return self.statcache[wpath]
531 except KeyError:
531 except KeyError:
532 try:
532 try:
533 return self.stat(wpath)
533 return self.stat(wpath)
534 except OSError, err:
534 except OSError, err:
535 if err.errno != errno.ENOENT:
535 if err.errno != errno.ENOENT:
536 raise
536 raise
537
537
538 def stat(self, wpath):
538 def stat(self, wpath):
539 try:
539 try:
540 st = os.lstat(join(self.wprefix, wpath))
540 st = os.lstat(join(self.wprefix, wpath))
541 ret = st.st_mode, st.st_size, st.st_mtime
541 ret = st.st_mode, st.st_size, st.st_mtime
542 self.statcache[wpath] = ret
542 self.statcache[wpath] = ret
543 return ret
543 return ret
544 except OSError:
544 except OSError:
545 self.statcache.pop(wpath, None)
545 self.statcache.pop(wpath, None)
546 raise
546 raise
547
547
548 @eventaction('c')
548 @eventaction('c')
549 def created(self, wpath):
549 def created(self, wpath):
550 if wpath == '.hgignore':
550 if wpath == '.hgignore':
551 self.update_hgignore()
551 self.update_hgignore()
552 try:
552 try:
553 st = self.stat(wpath)
553 st = self.stat(wpath)
554 if stat.S_ISREG(st[0]):
554 if stat.S_ISREG(st[0]):
555 self.updatefile(wpath, st)
555 self.updatefile(wpath, st)
556 except OSError:
556 except OSError:
557 pass
557 pass
558
558
559 @eventaction('m')
559 @eventaction('m')
560 def modified(self, wpath):
560 def modified(self, wpath):
561 if wpath == '.hgignore':
561 if wpath == '.hgignore':
562 self.update_hgignore()
562 self.update_hgignore()
563 try:
563 try:
564 st = self.stat(wpath)
564 st = self.stat(wpath)
565 if stat.S_ISREG(st[0]):
565 if stat.S_ISREG(st[0]):
566 if self.dirstate[wpath] in 'lmn':
566 if self.dirstate[wpath] in 'lmn':
567 self.updatefile(wpath, st)
567 self.updatefile(wpath, st)
568 except OSError:
568 except OSError:
569 pass
569 pass
570
570
571 @eventaction('d')
571 @eventaction('d')
572 def deleted(self, wpath):
572 def deleted(self, wpath):
573 if wpath == '.hgignore':
573 if wpath == '.hgignore':
574 self.update_hgignore()
574 self.update_hgignore()
575 elif wpath.startswith('.hg/'):
575 elif wpath.startswith('.hg/'):
576 if wpath == '.hg/wlock':
576 if wpath == '.hg/wlock':
577 self.check_dirstate()
577 self.check_dirstate()
578 return
578 return
579
579
580 self.deletefile(wpath, self.dirstate[wpath])
580 self.deletefile(wpath, self.dirstate[wpath])
581
581
582 def process_create(self, wpath, evt):
582 def process_create(self, wpath, evt):
583 if self.ui.debugflag:
583 if self.ui.debugflag:
584 self.ui.note(_('%s event: created %s\n') %
584 self.ui.note(_('%s event: created %s\n') %
585 (self.event_time(), wpath))
585 (self.event_time(), wpath))
586
586
587 if evt.mask & inotify.IN_ISDIR:
587 if evt.mask & inotify.IN_ISDIR:
588 self.scan(wpath)
588 self.scan(wpath)
589 else:
589 else:
590 self.created(wpath)
590 self.created(wpath)
591
591
592 def process_delete(self, wpath, evt):
592 def process_delete(self, wpath, evt):
593 if self.ui.debugflag:
593 if self.ui.debugflag:
594 self.ui.note(_('%s event: deleted %s\n') %
594 self.ui.note(_('%s event: deleted %s\n') %
595 (self.event_time(), wpath))
595 (self.event_time(), wpath))
596
596
597 if evt.mask & inotify.IN_ISDIR:
597 if evt.mask & inotify.IN_ISDIR:
598 tree = self.tree.dir(wpath)
598 tree = self.tree.dir(wpath)
599 todelete = [wfn for wfn, ignore in tree.walk('?')]
599 todelete = [wfn for wfn, ignore in tree.walk('?')]
600 for fn in todelete:
600 for fn in todelete:
601 self.deletefile(fn, '?')
601 self.deletefile(fn, '?')
602 self.scan(wpath)
602 self.scan(wpath)
603 else:
603 else:
604 self.deleted(wpath)
604 self.deleted(wpath)
605
605
606 def process_modify(self, wpath, evt):
606 def process_modify(self, wpath, evt):
607 if self.ui.debugflag:
607 if self.ui.debugflag:
608 self.ui.note(_('%s event: modified %s\n') %
608 self.ui.note(_('%s event: modified %s\n') %
609 (self.event_time(), wpath))
609 (self.event_time(), wpath))
610
610
611 if not (evt.mask & inotify.IN_ISDIR):
611 if not (evt.mask & inotify.IN_ISDIR):
612 self.modified(wpath)
612 self.modified(wpath)
613
613
614 def process_unmount(self, evt):
614 def process_unmount(self, evt):
615 self.ui.warn(_('filesystem containing %s was unmounted\n') %
615 self.ui.warn(_('filesystem containing %s was unmounted\n') %
616 evt.fullpath)
616 evt.fullpath)
617 sys.exit(0)
617 sys.exit(0)
618
618
619 def handle_pollevents(self, events):
619 def handle_pollevents(self, events):
620 if self.ui.debugflag:
620 if self.ui.debugflag:
621 self.ui.note(_('%s readable: %d bytes\n') %
621 self.ui.note(_('%s readable: %d bytes\n') %
622 (self.event_time(), self.threshold.readable()))
622 (self.event_time(), self.threshold.readable()))
623 if not self.threshold():
623 if not self.threshold():
624 if self.registered:
624 if self.registered:
625 if self.ui.debugflag:
625 if self.ui.debugflag:
626 self.ui.note(_('%s below threshold - unhooking\n') %
626 self.ui.note(_('%s below threshold - unhooking\n') %
627 (self.event_time()))
627 (self.event_time()))
628 self.unregister()
628 self.unregister()
629 self.timeout = 250
629 self.timeout = 250
630 else:
630 else:
631 self.read_events()
631 self.read_events()
632
632
633 def read_events(self, bufsize=None):
633 def read_events(self, bufsize=None):
634 events = self.watcher.read(bufsize)
634 events = self.watcher.read(bufsize)
635 if self.ui.debugflag:
635 if self.ui.debugflag:
636 self.ui.note(_('%s reading %d events\n') %
636 self.ui.note(_('%s reading %d events\n') %
637 (self.event_time(), len(events)))
637 (self.event_time(), len(events)))
638 for evt in events:
638 for evt in events:
639 assert evt.fullpath.startswith(self.wprefix)
639 assert evt.fullpath.startswith(self.wprefix)
640 wpath = evt.fullpath[self.prefixlen:]
640 wpath = evt.fullpath[self.prefixlen:]
641
641
642 # paths have been normalized, wpath never ends with a '/'
642 # paths have been normalized, wpath never ends with a '/'
643
643
644 if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR:
644 if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR:
645 # ignore subdirectories of .hg/ (merge, patches...)
645 # ignore subdirectories of .hg/ (merge, patches...)
646 continue
646 continue
647
647
648 if evt.mask & inotify.IN_UNMOUNT:
648 if evt.mask & inotify.IN_UNMOUNT:
649 self.process_unmount(wpath, evt)
649 self.process_unmount(wpath, evt)
650 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
650 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
651 self.process_modify(wpath, evt)
651 self.process_modify(wpath, evt)
652 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
652 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
653 inotify.IN_MOVED_FROM):
653 inotify.IN_MOVED_FROM):
654 self.process_delete(wpath, evt)
654 self.process_delete(wpath, evt)
655 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
655 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
656 self.process_create(wpath, evt)
656 self.process_create(wpath, evt)
657
657
658 self.lastevent.clear()
658 self.lastevent.clear()
659
659
660 def handle_timeout(self):
660 def handle_timeout(self):
661 if not self.registered:
661 if not self.registered:
662 if self.ui.debugflag:
662 if self.ui.debugflag:
663 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
663 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
664 (self.event_time(), self.threshold.readable()))
664 (self.event_time(), self.threshold.readable()))
665 self.read_events(0)
665 self.read_events(0)
666 self.register(timeout=None)
666 self.register(timeout=None)
667
667
668 self.timeout = None
668 self.timeout = None
669
669
670 def shutdown(self):
670 def shutdown(self):
671 self.watcher.close()
671 self.watcher.close()
672
672
673 def debug(self):
673 def debug(self):
674 """
674 """
675 Returns a sorted list of relatives paths currently watched,
675 Returns a sorted list of relatives paths currently watched,
676 for debugging purposes.
676 for debugging purposes.
677 """
677 """
678 return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher)
678 return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher)
679
679
680 class server(pollable):
680 class server(pollable):
681 """
681 """
682 Listens for client queries on unix socket inotify.sock
682 Listens for client queries on unix socket inotify.sock
683 """
683 """
684 def __init__(self, ui, root, repowatcher, timeout):
684 def __init__(self, ui, root, repowatcher, timeout):
685 self.ui = ui
685 self.ui = ui
686 self.repowatcher = repowatcher
686 self.repowatcher = repowatcher
687 self.sock = socket.socket(socket.AF_UNIX)
687 self.sock = socket.socket(socket.AF_UNIX)
688 self.sockpath = join(root, '.hg/inotify.sock')
688 self.sockpath = join(root, '.hg/inotify.sock')
689 self.realsockpath = None
689 self.realsockpath = None
690 try:
690 try:
691 self.sock.bind(self.sockpath)
691 self.sock.bind(self.sockpath)
692 except socket.error, err:
692 except socket.error, err:
693 if err[0] == errno.EADDRINUSE:
693 if err[0] == errno.EADDRINUSE:
694 raise AlreadyStartedException(_('could not start server: %s')
694 raise AlreadyStartedException( _('cannot start: socket is '
695 % err[1])
695 'already bound'))
696 if err[0] == "AF_UNIX path too long":
696 if err[0] == "AF_UNIX path too long":
697 if os.path.islink(self.sockpath) and \
698 not os.path.exists(self.sockpath):
699 raise util.Abort('inotify-server: cannot start: '
700 '.hg/inotify.sock is a broken symlink')
697 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
701 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
698 self.realsockpath = os.path.join(tempdir, "inotify.sock")
702 self.realsockpath = os.path.join(tempdir, "inotify.sock")
699 try:
703 try:
700 self.sock.bind(self.realsockpath)
704 self.sock.bind(self.realsockpath)
701 os.symlink(self.realsockpath, self.sockpath)
705 os.symlink(self.realsockpath, self.sockpath)
702 except (OSError, socket.error), inst:
706 except (OSError, socket.error), inst:
703 try:
707 try:
704 os.unlink(self.realsockpath)
708 os.unlink(self.realsockpath)
705 except:
709 except:
706 pass
710 pass
707 os.rmdir(tempdir)
711 os.rmdir(tempdir)
708 if inst.errno == errno.EEXIST:
712 if inst.errno == errno.EEXIST:
709 raise AlreadyStartedException(_('could not start server: %s')
713 raise AlreadyStartedException(_('cannot start: tried '
710 % inst.strerror)
714 'linking .hg/inotify.sock to a temporary socket but'
715 ' .hg/inotify.sock already exists'))
711 raise
716 raise
712 else:
717 else:
713 raise
718 raise
714 self.sock.listen(5)
719 self.sock.listen(5)
715 self.fileno = self.sock.fileno
720 self.fileno = self.sock.fileno
716 self.register(timeout=timeout)
721 self.register(timeout=timeout)
717
722
718 def handle_timeout(self):
723 def handle_timeout(self):
719 pass
724 pass
720
725
721 def answer_stat_query(self, cs):
726 def answer_stat_query(self, cs):
722 names = cs.read().split('\0')
727 names = cs.read().split('\0')
723
728
724 states = names.pop()
729 states = names.pop()
725
730
726 self.ui.note(_('answering query for %r\n') % states)
731 self.ui.note(_('answering query for %r\n') % states)
727
732
728 if self.repowatcher.timeout:
733 if self.repowatcher.timeout:
729 # We got a query while a rescan is pending. Make sure we
734 # We got a query while a rescan is pending. Make sure we
730 # rescan before responding, or we could give back a wrong
735 # rescan before responding, or we could give back a wrong
731 # answer.
736 # answer.
732 self.repowatcher.handle_timeout()
737 self.repowatcher.handle_timeout()
733
738
734 visited = set()
739 visited = set()
735 if not names:
740 if not names:
736 def genresult(states, tree):
741 def genresult(states, tree):
737 for fn, state in tree.walk(states):
742 for fn, state in tree.walk(states):
738 yield fn
743 yield fn
739 else:
744 else:
740 def genresult(states, tree):
745 def genresult(states, tree):
741 for fn in names:
746 for fn in names:
742 for f in tree.lookup(states, fn, visited):
747 for f in tree.lookup(states, fn, visited):
743 yield f
748 yield f
744
749
745 return ['\0'.join(r) for r in [
750 return ['\0'.join(r) for r in [
746 genresult('l', self.repowatcher.statustrees['l']),
751 genresult('l', self.repowatcher.statustrees['l']),
747 genresult('m', self.repowatcher.statustrees['m']),
752 genresult('m', self.repowatcher.statustrees['m']),
748 genresult('a', self.repowatcher.statustrees['a']),
753 genresult('a', self.repowatcher.statustrees['a']),
749 genresult('r', self.repowatcher.statustrees['r']),
754 genresult('r', self.repowatcher.statustrees['r']),
750 genresult('!', self.repowatcher.statustrees['!']),
755 genresult('!', self.repowatcher.statustrees['!']),
751 '?' in states
756 '?' in states
752 and genresult('?', self.repowatcher.statustrees['?'])
757 and genresult('?', self.repowatcher.statustrees['?'])
753 or [],
758 or [],
754 [],
759 [],
755 'c' in states and genresult('n', self.repowatcher.tree) or [],
760 'c' in states and genresult('n', self.repowatcher.tree) or [],
756 visited
761 visited
757 ]]
762 ]]
758
763
759 def answer_dbug_query(self):
764 def answer_dbug_query(self):
760 return ['\0'.join(self.repowatcher.debug())]
765 return ['\0'.join(self.repowatcher.debug())]
761
766
762 def handle_pollevents(self, events):
767 def handle_pollevents(self, events):
763 for e in events:
768 for e in events:
764 self.handle_pollevent()
769 self.handle_pollevent()
765
770
766 def handle_pollevent(self):
771 def handle_pollevent(self):
767 sock, addr = self.sock.accept()
772 sock, addr = self.sock.accept()
768
773
769 cs = common.recvcs(sock)
774 cs = common.recvcs(sock)
770 version = ord(cs.read(1))
775 version = ord(cs.read(1))
771
776
772 if version != common.version:
777 if version != common.version:
773 self.ui.warn(_('received query from incompatible client '
778 self.ui.warn(_('received query from incompatible client '
774 'version %d\n') % version)
779 'version %d\n') % version)
775 try:
780 try:
776 # try to send back our version to the client
781 # try to send back our version to the client
777 # this way, the client too is informed of the mismatch
782 # this way, the client too is informed of the mismatch
778 sock.sendall(chr(common.version))
783 sock.sendall(chr(common.version))
779 except:
784 except:
780 pass
785 pass
781 return
786 return
782
787
783 type = cs.read(4)
788 type = cs.read(4)
784
789
785 if type == 'STAT':
790 if type == 'STAT':
786 results = self.answer_stat_query(cs)
791 results = self.answer_stat_query(cs)
787 elif type == 'DBUG':
792 elif type == 'DBUG':
788 results = self.answer_dbug_query()
793 results = self.answer_dbug_query()
789 else:
794 else:
790 self.ui.warn(_('unrecognized query type: %s\n') % type)
795 self.ui.warn(_('unrecognized query type: %s\n') % type)
791 return
796 return
792
797
793 try:
798 try:
794 try:
799 try:
795 v = chr(common.version)
800 v = chr(common.version)
796
801
797 sock.sendall(v + type + struct.pack(common.resphdrfmts[type],
802 sock.sendall(v + type + struct.pack(common.resphdrfmts[type],
798 *map(len, results)))
803 *map(len, results)))
799 sock.sendall(''.join(results))
804 sock.sendall(''.join(results))
800 finally:
805 finally:
801 sock.shutdown(socket.SHUT_WR)
806 sock.shutdown(socket.SHUT_WR)
802 except socket.error, err:
807 except socket.error, err:
803 if err[0] != errno.EPIPE:
808 if err[0] != errno.EPIPE:
804 raise
809 raise
805
810
806 def shutdown(self):
811 def shutdown(self):
807 self.sock.close()
812 self.sock.close()
808 try:
813 try:
809 os.unlink(self.sockpath)
814 os.unlink(self.sockpath)
810 if self.realsockpath:
815 if self.realsockpath:
811 os.unlink(self.realsockpath)
816 os.unlink(self.realsockpath)
812 os.rmdir(os.path.dirname(self.realsockpath))
817 os.rmdir(os.path.dirname(self.realsockpath))
813 except OSError, err:
818 except OSError, err:
814 if err.errno != errno.ENOENT:
819 if err.errno != errno.ENOENT:
815 raise
820 raise
816
821
817 class master(object):
822 class master(object):
818 def __init__(self, ui, dirstate, root, timeout=None):
823 def __init__(self, ui, dirstate, root, timeout=None):
819 self.ui = ui
824 self.ui = ui
820 self.repowatcher = repowatcher(ui, dirstate, root)
825 self.repowatcher = repowatcher(ui, dirstate, root)
821 self.server = server(ui, root, self.repowatcher, timeout)
826 self.server = server(ui, root, self.repowatcher, timeout)
822
827
823 def shutdown(self):
828 def shutdown(self):
824 for obj in pollable.instances.itervalues():
829 for obj in pollable.instances.itervalues():
825 obj.shutdown()
830 obj.shutdown()
826
831
827 def run(self):
832 def run(self):
828 self.repowatcher.setup()
833 self.repowatcher.setup()
829 self.ui.note(_('finished setup\n'))
834 self.ui.note(_('finished setup\n'))
830 if os.getenv('TIME_STARTUP'):
835 if os.getenv('TIME_STARTUP'):
831 sys.exit(0)
836 sys.exit(0)
832 pollable.run()
837 pollable.run()
833
838
834 def start(ui, dirstate, root, opts):
839 def start(ui, dirstate, root, opts):
835 timeout = opts.get('timeout')
840 timeout = opts.get('timeout')
836 if timeout:
841 if timeout:
837 timeout = float(timeout) * 1e3
842 timeout = float(timeout) * 1e3
838
843
839 class service(object):
844 class service(object):
840 def init(self):
845 def init(self):
841 try:
846 try:
842 self.master = master(ui, dirstate, root, timeout)
847 self.master = master(ui, dirstate, root, timeout)
843 except AlreadyStartedException, inst:
848 except AlreadyStartedException, inst:
844 raise util.Abort(str(inst))
849 raise util.Abort("inotify-server: %s" % inst)
845
850
846 def run(self):
851 def run(self):
847 try:
852 try:
848 self.master.run()
853 self.master.run()
849 finally:
854 finally:
850 self.master.shutdown()
855 self.master.shutdown()
851
856
852 if 'inserve' not in sys.argv:
857 if 'inserve' not in sys.argv:
853 runargs = [sys.argv[0], 'inserve', '-R', root]
858 runargs = [sys.argv[0], 'inserve', '-R', root]
854 else:
859 else:
855 runargs = sys.argv[:]
860 runargs = sys.argv[:]
856
861
857 pidfile = ui.config('inotify', 'pidfile')
862 pidfile = ui.config('inotify', 'pidfile')
858 if opts['daemon'] and pidfile is not None and 'pid-file' not in runargs:
863 if opts['daemon'] and pidfile is not None and 'pid-file' not in runargs:
859 runargs.append("--pid-file=%s" % pidfile)
864 runargs.append("--pid-file=%s" % pidfile)
860
865
861 service = service()
866 service = service()
862 logfile = ui.config('inotify', 'log')
867 logfile = ui.config('inotify', 'log')
863 cmdutil.service(opts, initfn=service.init, runfn=service.run,
868 cmdutil.service(opts, initfn=service.init, runfn=service.run,
864 logfile=logfile, runargs=runargs)
869 logfile=logfile, runargs=runargs)
@@ -1,96 +1,100
1 #!/bin/sh
1 #!/bin/sh
2
2
3 "$TESTDIR/hghave" inotify || exit 80
3 "$TESTDIR/hghave" inotify || exit 80
4
4
5 hg init repo1
5 hg init repo1
6 cd repo1
6 cd repo1
7
7
8 touch a b c d e
8 touch a b c d e
9 mkdir dir
9 mkdir dir
10 mkdir dir/bar
10 mkdir dir/bar
11 touch dir/x dir/y dir/bar/foo
11 touch dir/x dir/y dir/bar/foo
12
12
13 hg ci -Am m
13 hg ci -Am m
14 cd ..
14 cd ..
15 hg clone repo1 repo2
15 hg clone repo1 repo2
16
16
17 echo "[extensions]" >> $HGRCPATH
17 echo "[extensions]" >> $HGRCPATH
18 echo "inotify=" >> $HGRCPATH
18 echo "inotify=" >> $HGRCPATH
19
19
20 cd repo2
20 cd repo2
21 echo b >> a
21 echo b >> a
22 # check that daemon started automatically works correctly
22 # check that daemon started automatically works correctly
23 # and make sure that inotify.pidfile works
23 # and make sure that inotify.pidfile works
24 hg --config "inotify.pidfile=../hg2.pid" status
24 hg --config "inotify.pidfile=../hg2.pid" status
25
25
26 # make sure that pidfile worked. Output should be silent.
26 # make sure that pidfile worked. Output should be silent.
27 kill `cat ../hg2.pid`
27 kill `cat ../hg2.pid`
28
28
29 cd ../repo1
29 cd ../repo1
30 echo % inserve
30 echo % inserve
31 hg inserve -d --pid-file=hg.pid
31 hg inserve -d --pid-file=hg.pid
32 cat hg.pid >> "$DAEMON_PIDS"
32 cat hg.pid >> "$DAEMON_PIDS"
33
33
34 # let the daemon finish its stuff
34 # let the daemon finish its stuff
35 sleep 1
35 sleep 1
36
37 echo % cannot start, already bound
38 hg inserve
39
36 # issue907
40 # issue907
37 hg status
41 hg status
38 echo % clean
42 echo % clean
39 hg status -c
43 hg status -c
40 echo % all
44 echo % all
41 hg status -A
45 hg status -A
42
46
43 echo '% path patterns'
47 echo '% path patterns'
44 echo x > dir/x
48 echo x > dir/x
45 hg status .
49 hg status .
46 hg status dir
50 hg status dir
47 cd dir
51 cd dir
48 hg status .
52 hg status .
49 cd ..
53 cd ..
50
54
51 #issue 1375
55 #issue 1375
52 #Testing that we can remove a folder and then add a file with the same name
56 #Testing that we can remove a folder and then add a file with the same name
53 echo % issue 1375
57 echo % issue 1375
54
58
55 mkdir h
59 mkdir h
56 echo h > h/h
60 echo h > h/h
57 hg ci -Am t
61 hg ci -Am t
58 hg rm h
62 hg rm h
59
63
60 echo h >h
64 echo h >h
61 hg add h
65 hg add h
62
66
63 hg status
67 hg status
64 hg ci -m0
68 hg ci -m0
65
69
66 # Test for issue1735: inotify watches files in .hg/merge
70 # Test for issue1735: inotify watches files in .hg/merge
67 hg st
71 hg st
68
72
69 echo a > a
73 echo a > a
70
74
71 hg ci -Am a
75 hg ci -Am a
72 hg st
76 hg st
73
77
74 echo b >> a
78 echo b >> a
75 hg ci -m ab
79 hg ci -m ab
76 hg st
80 hg st
77
81
78 echo c >> a
82 echo c >> a
79 hg st
83 hg st
80
84
81 hg up 0
85 hg up 0
82 hg st
86 hg st
83
87
84 HGMERGE=internal:local hg up
88 HGMERGE=internal:local hg up
85 hg st
89 hg st
86
90
87 # Test for 1844: "hg ci folder" will not commit all changes beneath "folder"
91 # Test for 1844: "hg ci folder" will not commit all changes beneath "folder"
88 mkdir 1844
92 mkdir 1844
89 echo a > 1844/foo
93 echo a > 1844/foo
90 hg add 1844
94 hg add 1844
91 hg ci -m 'working'
95 hg ci -m 'working'
92
96
93 echo b >> 1844/foo
97 echo b >> 1844/foo
94 hg ci 1844 -m 'broken'
98 hg ci 1844 -m 'broken'
95
99
96 kill `cat hg.pid`
100 kill `cat hg.pid`
@@ -1,7 +1,7
1 % fail
1 % fail
2 abort: could not start server: File exists
2 abort: inotify-server: cannot start: .hg/inotify.sock is a broken symlink
3 could not talk to new inotify server: No such file or directory
3 inotify-client: could not talk to new inotify server: No such file or directory
4 abort: could not start server: File exists
4 abort: inotify-server: cannot start: .hg/inotify.sock is a broken symlink
5 % inserve
5 % inserve
6 % status
6 % status
7 ? hg.pid
7 ? hg.pid
@@ -1,50 +1,52
1 adding a
1 adding a
2 adding b
2 adding b
3 adding c
3 adding c
4 adding d
4 adding d
5 adding dir/bar/foo
5 adding dir/bar/foo
6 adding dir/x
6 adding dir/x
7 adding dir/y
7 adding dir/y
8 adding e
8 adding e
9 updating to branch default
9 updating to branch default
10 8 files updated, 0 files merged, 0 files removed, 0 files unresolved
10 8 files updated, 0 files merged, 0 files removed, 0 files unresolved
11 M a
11 M a
12 % inserve
12 % inserve
13 % cannot start, already bound
14 abort: inotify-server: cannot start: socket is already bound
13 ? hg.pid
15 ? hg.pid
14 % clean
16 % clean
15 C a
17 C a
16 C b
18 C b
17 C c
19 C c
18 C d
20 C d
19 C dir/bar/foo
21 C dir/bar/foo
20 C dir/x
22 C dir/x
21 C dir/y
23 C dir/y
22 C e
24 C e
23 % all
25 % all
24 ? hg.pid
26 ? hg.pid
25 C a
27 C a
26 C b
28 C b
27 C c
29 C c
28 C d
30 C d
29 C dir/bar/foo
31 C dir/bar/foo
30 C dir/x
32 C dir/x
31 C dir/y
33 C dir/y
32 C e
34 C e
33 % path patterns
35 % path patterns
34 M dir/x
36 M dir/x
35 ? hg.pid
37 ? hg.pid
36 M dir/x
38 M dir/x
37 M x
39 M x
38 % issue 1375
40 % issue 1375
39 adding h/h
41 adding h/h
40 adding hg.pid
42 adding hg.pid
41 removing h/h
43 removing h/h
42 A h
44 A h
43 R h/h
45 R h/h
44 M a
46 M a
45 merging a
47 merging a
46 1 files updated, 1 files merged, 2 files removed, 0 files unresolved
48 1 files updated, 1 files merged, 2 files removed, 0 files unresolved
47 M a
49 M a
48 3 files updated, 1 files merged, 0 files removed, 0 files unresolved
50 3 files updated, 1 files merged, 0 files removed, 0 files unresolved
49 M a
51 M a
50 adding 1844/foo
52 adding 1844/foo
General Comments 0
You need to be logged in to leave comments. Login now