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