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