##// END OF EJS Templates
inotify/inserve: implement --timeout-idle option (issue885)...
Benoit Boissinot -
r10494:08064db9 stable
parent child Browse files
Show More
@@ -1,441 +1,441 b''
1 1 # linuxserver.py - inotify status server 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 or any later version.
8 8
9 9 from mercurial.i18n import _
10 10 from mercurial import osutil, util
11 11 import server
12 12 import errno, os, select, stat, sys, 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 def walkrepodirs(dirstate, absroot):
21 21 '''Iterate over all subdirectories of this repo.
22 22 Exclude the .hg directory, any nested repos, and ignored dirs.'''
23 23 def walkit(dirname, top):
24 24 fullpath = server.join(absroot, dirname)
25 25 try:
26 26 for name, kind in osutil.listdir(fullpath):
27 27 if kind == stat.S_IFDIR:
28 28 if name == '.hg':
29 29 if not top:
30 30 return
31 31 else:
32 32 d = server.join(dirname, name)
33 33 if dirstate._ignore(d):
34 34 continue
35 35 for subdir in walkit(d, False):
36 36 yield subdir
37 37 except OSError, err:
38 38 if err.errno not in server.walk_ignored_errors:
39 39 raise
40 40 yield fullpath
41 41
42 42 return walkit('', True)
43 43
44 44 def _explain_watch_limit(ui, dirstate, rootabs):
45 45 path = '/proc/sys/fs/inotify/max_user_watches'
46 46 try:
47 47 limit = int(file(path).read())
48 48 except IOError, err:
49 49 if err.errno != errno.ENOENT:
50 50 raise
51 51 raise util.Abort(_('this system does not seem to '
52 52 'support inotify'))
53 53 ui.warn(_('*** the current per-user limit on the number '
54 54 'of inotify watches is %s\n') % limit)
55 55 ui.warn(_('*** this limit is too low to watch every '
56 56 'directory in this repository\n'))
57 57 ui.warn(_('*** counting directories: '))
58 58 ndirs = len(list(walkrepodirs(dirstate, rootabs)))
59 59 ui.warn(_('found %d\n') % ndirs)
60 60 newlimit = min(limit, 1024)
61 61 while newlimit < ((limit + ndirs) * 1.1):
62 62 newlimit *= 2
63 63 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
64 64 (limit, newlimit))
65 65 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
66 66 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
67 67 % rootabs)
68 68
69 69 class pollable(object):
70 70 """
71 71 Interface to support polling.
72 72 The file descriptor returned by fileno() is registered to a polling
73 73 object.
74 74 Usage:
75 75 Every tick, check if an event has happened since the last tick:
76 76 * If yes, call handle_events
77 77 * If no, call handle_timeout
78 78 """
79 79 poll_events = select.POLLIN
80 80 instances = {}
81 81 poll = select.poll()
82 82
83 83 def fileno(self):
84 84 raise NotImplementedError
85 85
86 86 def handle_events(self, events):
87 87 raise NotImplementedError
88 88
89 89 def handle_timeout(self):
90 90 raise NotImplementedError
91 91
92 92 def shutdown(self):
93 93 raise NotImplementedError
94 94
95 95 def register(self, timeout):
96 96 fd = self.fileno()
97 97
98 98 pollable.poll.register(fd, pollable.poll_events)
99 99 pollable.instances[fd] = self
100 100
101 101 self.registered = True
102 102 self.timeout = timeout
103 103
104 104 def unregister(self):
105 105 pollable.poll.unregister(self)
106 106 self.registered = False
107 107
108 108 @classmethod
109 109 def run(cls):
110 110 while True:
111 111 timeout = None
112 112 timeobj = None
113 113 for obj in cls.instances.itervalues():
114 114 if obj.timeout is not None and (timeout is None
115 115 or obj.timeout < timeout):
116 116 timeout, timeobj = obj.timeout, obj
117 117 try:
118 118 events = cls.poll.poll(timeout)
119 119 except select.error, err:
120 120 if err[0] == errno.EINTR:
121 121 continue
122 122 raise
123 123 if events:
124 124 by_fd = {}
125 125 for fd, event in events:
126 126 by_fd.setdefault(fd, []).append(event)
127 127
128 128 for fd, events in by_fd.iteritems():
129 129 cls.instances[fd].handle_pollevents(events)
130 130
131 131 elif timeobj:
132 132 timeobj.handle_timeout()
133 133
134 134 def eventaction(code):
135 135 """
136 136 Decorator to help handle events in repowatcher
137 137 """
138 138 def decorator(f):
139 139 def wrapper(self, wpath):
140 140 if code == 'm' and wpath in self.lastevent and \
141 141 self.lastevent[wpath] in 'cm':
142 142 return
143 143 self.lastevent[wpath] = code
144 144 self.timeout = 250
145 145
146 146 f(self, wpath)
147 147
148 148 wrapper.func_name = f.func_name
149 149 return wrapper
150 150 return decorator
151 151
152 152 class repowatcher(server.repowatcher, pollable):
153 153 """
154 154 Watches inotify events
155 155 """
156 156 mask = (
157 157 inotify.IN_ATTRIB |
158 158 inotify.IN_CREATE |
159 159 inotify.IN_DELETE |
160 160 inotify.IN_DELETE_SELF |
161 161 inotify.IN_MODIFY |
162 162 inotify.IN_MOVED_FROM |
163 163 inotify.IN_MOVED_TO |
164 164 inotify.IN_MOVE_SELF |
165 165 inotify.IN_ONLYDIR |
166 166 inotify.IN_UNMOUNT |
167 167 0)
168 168
169 169 def __init__(self, ui, dirstate, root):
170 170 server.repowatcher.__init__(self, ui, dirstate, root)
171 171
172 172 self.lastevent = {}
173 173 self.dirty = False
174 174 try:
175 175 self.watcher = watcher.watcher()
176 176 except OSError, err:
177 177 raise util.Abort(_('inotify service not available: %s') %
178 178 err.strerror)
179 179 self.threshold = watcher.threshold(self.watcher)
180 180 self.fileno = self.watcher.fileno
181 181 self.register(timeout=None)
182 182
183 183 self.handle_timeout()
184 184 self.scan()
185 185
186 186 def event_time(self):
187 187 last = self.last_event
188 188 now = time.time()
189 189 self.last_event = now
190 190
191 191 if last is None:
192 192 return 'start'
193 193 delta = now - last
194 194 if delta < 5:
195 195 return '+%.3f' % delta
196 196 if delta < 50:
197 197 return '+%.2f' % delta
198 198 return '+%.1f' % delta
199 199
200 200 def add_watch(self, path, mask):
201 201 if not path:
202 202 return
203 203 if self.watcher.path(path) is None:
204 204 if self.ui.debugflag:
205 205 self.ui.note(_('watching %r\n') % path[self.prefixlen:])
206 206 try:
207 207 self.watcher.add(path, mask)
208 208 except OSError, err:
209 209 if err.errno in (errno.ENOENT, errno.ENOTDIR):
210 210 return
211 211 if err.errno != errno.ENOSPC:
212 212 raise
213 213 _explain_watch_limit(self.ui, self.dirstate, self.wprefix)
214 214
215 215 def setup(self):
216 216 self.ui.note(_('watching directories under %r\n') % self.wprefix)
217 217 self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE)
218 218
219 219 def scan(self, topdir=''):
220 220 ds = self.dirstate._map.copy()
221 221 self.add_watch(server.join(self.wprefix, topdir), self.mask)
222 222 for root, dirs, files in server.walk(self.dirstate, self.wprefix,
223 223 topdir):
224 224 for d in dirs:
225 225 self.add_watch(server.join(root, d), self.mask)
226 226 wroot = root[self.prefixlen:]
227 227 for fn in files:
228 228 wfn = server.join(wroot, fn)
229 229 self.updatefile(wfn, self.getstat(wfn))
230 230 ds.pop(wfn, None)
231 231 wtopdir = topdir
232 232 if wtopdir and wtopdir[-1] != '/':
233 233 wtopdir += '/'
234 234 for wfn, state in ds.iteritems():
235 235 if not wfn.startswith(wtopdir):
236 236 continue
237 237 try:
238 238 st = self.stat(wfn)
239 239 except OSError:
240 240 status = state[0]
241 241 self.deletefile(wfn, status)
242 242 else:
243 243 self.updatefile(wfn, st)
244 244 self.check_deleted('!')
245 245 self.check_deleted('r')
246 246
247 247 @eventaction('c')
248 248 def created(self, wpath):
249 249 if wpath == '.hgignore':
250 250 self.update_hgignore()
251 251 try:
252 252 st = self.stat(wpath)
253 253 if stat.S_ISREG(st[0]) or stat.S_ISLNK(st[0]):
254 254 self.updatefile(wpath, st)
255 255 except OSError:
256 256 pass
257 257
258 258 @eventaction('m')
259 259 def modified(self, wpath):
260 260 if wpath == '.hgignore':
261 261 self.update_hgignore()
262 262 try:
263 263 st = self.stat(wpath)
264 264 if stat.S_ISREG(st[0]):
265 265 if self.dirstate[wpath] in 'lmn':
266 266 self.updatefile(wpath, st)
267 267 except OSError:
268 268 pass
269 269
270 270 @eventaction('d')
271 271 def deleted(self, wpath):
272 272 if wpath == '.hgignore':
273 273 self.update_hgignore()
274 274 elif wpath.startswith('.hg/'):
275 275 return
276 276
277 277 self.deletefile(wpath, self.dirstate[wpath])
278 278
279 279 def process_create(self, wpath, evt):
280 280 if self.ui.debugflag:
281 281 self.ui.note(_('%s event: created %s\n') %
282 282 (self.event_time(), wpath))
283 283
284 284 if evt.mask & inotify.IN_ISDIR:
285 285 self.scan(wpath)
286 286 else:
287 287 self.created(wpath)
288 288
289 289 def process_delete(self, wpath, evt):
290 290 if self.ui.debugflag:
291 291 self.ui.note(_('%s event: deleted %s\n') %
292 292 (self.event_time(), wpath))
293 293
294 294 if evt.mask & inotify.IN_ISDIR:
295 295 tree = self.tree.dir(wpath)
296 296 todelete = [wfn for wfn, ignore in tree.walk('?')]
297 297 for fn in todelete:
298 298 self.deletefile(fn, '?')
299 299 self.scan(wpath)
300 300 else:
301 301 self.deleted(wpath)
302 302
303 303 def process_modify(self, wpath, evt):
304 304 if self.ui.debugflag:
305 305 self.ui.note(_('%s event: modified %s\n') %
306 306 (self.event_time(), wpath))
307 307
308 308 if not (evt.mask & inotify.IN_ISDIR):
309 309 self.modified(wpath)
310 310
311 311 def process_unmount(self, evt):
312 312 self.ui.warn(_('filesystem containing %s was unmounted\n') %
313 313 evt.fullpath)
314 314 sys.exit(0)
315 315
316 316 def handle_pollevents(self, events):
317 317 if self.ui.debugflag:
318 318 self.ui.note(_('%s readable: %d bytes\n') %
319 319 (self.event_time(), self.threshold.readable()))
320 320 if not self.threshold():
321 321 if self.registered:
322 322 if self.ui.debugflag:
323 323 self.ui.note(_('%s below threshold - unhooking\n') %
324 324 (self.event_time()))
325 325 self.unregister()
326 326 self.timeout = 250
327 327 else:
328 328 self.read_events()
329 329
330 330 def read_events(self, bufsize=None):
331 331 events = self.watcher.read(bufsize)
332 332 if self.ui.debugflag:
333 333 self.ui.note(_('%s reading %d events\n') %
334 334 (self.event_time(), len(events)))
335 335 for evt in events:
336 336 if evt.fullpath == self.wprefix[:-1]:
337 337 # events on the root of the repository
338 338 # itself, e.g. permission changes or repository move
339 339 continue
340 340 assert evt.fullpath.startswith(self.wprefix)
341 341 wpath = evt.fullpath[self.prefixlen:]
342 342
343 343 # paths have been normalized, wpath never ends with a '/'
344 344
345 345 if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR:
346 346 # ignore subdirectories of .hg/ (merge, patches...)
347 347 continue
348 348 if wpath == ".hg/wlock":
349 349 if evt.mask & inotify.IN_DELETE:
350 350 self.dirstate.invalidate()
351 351 self.dirty = False
352 352 self.scan()
353 353 elif evt.mask & inotify.IN_CREATE:
354 354 self.dirty = True
355 355 else:
356 356 if self.dirty:
357 357 continue
358 358
359 359 if evt.mask & inotify.IN_UNMOUNT:
360 360 self.process_unmount(wpath, evt)
361 361 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
362 362 self.process_modify(wpath, evt)
363 363 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
364 364 inotify.IN_MOVED_FROM):
365 365 self.process_delete(wpath, evt)
366 366 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
367 367 self.process_create(wpath, evt)
368 368
369 369 self.lastevent.clear()
370 370
371 371 def handle_timeout(self):
372 372 if not self.registered:
373 373 if self.ui.debugflag:
374 374 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
375 375 (self.event_time(), self.threshold.readable()))
376 376 self.read_events(0)
377 377 self.register(timeout=None)
378 378
379 379 self.timeout = None
380 380
381 381 def shutdown(self):
382 382 self.watcher.close()
383 383
384 384 def debug(self):
385 385 """
386 386 Returns a sorted list of relatives paths currently watched,
387 387 for debugging purposes.
388 388 """
389 389 return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher)
390 390
391 391 class socketlistener(server.socketlistener, pollable):
392 392 """
393 393 Listens for client queries on unix socket inotify.sock
394 394 """
395 395 def __init__(self, ui, root, repowatcher, timeout):
396 396 server.socketlistener.__init__(self, ui, root, repowatcher, timeout)
397 397 self.register(timeout=timeout)
398 398
399 399 def handle_timeout(self):
400 pass
400 raise server.TimeoutException
401 401
402 402 def handle_pollevents(self, events):
403 403 for e in events:
404 404 self.accept_connection()
405 405
406 406 def shutdown(self):
407 407 self.sock.close()
408 408 try:
409 409 os.unlink(self.sockpath)
410 410 if self.realsockpath:
411 411 os.unlink(self.realsockpath)
412 412 os.rmdir(os.path.dirname(self.realsockpath))
413 413 except OSError, err:
414 414 if err.errno != errno.ENOENT:
415 415 raise
416 416
417 417 def answer_stat_query(self, cs):
418 418 if self.repowatcher.timeout:
419 419 # We got a query while a rescan is pending. Make sure we
420 420 # rescan before responding, or we could give back a wrong
421 421 # answer.
422 422 self.repowatcher.handle_timeout()
423 423 return server.socketlistener.answer_stat_query(self, cs)
424 424
425 425 class master(object):
426 426 def __init__(self, ui, dirstate, root, timeout=None):
427 427 self.ui = ui
428 428 self.repowatcher = repowatcher(ui, dirstate, root)
429 429 self.socketlistener = socketlistener(ui, root, self.repowatcher,
430 430 timeout)
431 431
432 432 def shutdown(self):
433 433 for obj in pollable.instances.itervalues():
434 434 obj.shutdown()
435 435
436 436 def run(self):
437 437 self.repowatcher.setup()
438 438 self.ui.note(_('finished setup\n'))
439 439 if os.getenv('TIME_STARTUP'):
440 440 sys.exit(0)
441 441 pollable.run()
@@ -1,479 +1,486 b''
1 1 # server.py - common entry point for inotify status server
2 2 #
3 3 # Copyright 2009 Nicolas Dumazet <nicdumz@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from mercurial.i18n import _
9 9 from mercurial import cmdutil, osutil, util
10 10 import common
11 11
12 12 import errno
13 13 import os
14 14 import socket
15 15 import stat
16 16 import struct
17 17 import sys
18 18 import tempfile
19 19
20 20 class AlreadyStartedException(Exception):
21 21 pass
22 class TimeoutException(Exception):
23 pass
22 24
23 25 def join(a, b):
24 26 if a:
25 27 if a[-1] == '/':
26 28 return a + b
27 29 return a + '/' + b
28 30 return b
29 31
30 32 def split(path):
31 33 c = path.rfind('/')
32 34 if c == -1:
33 35 return '', path
34 36 return path[:c], path[c + 1:]
35 37
36 38 walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
37 39
38 40 def walk(dirstate, absroot, root):
39 41 '''Like os.walk, but only yields regular files.'''
40 42
41 43 # This function is critical to performance during startup.
42 44
43 45 def walkit(root, reporoot):
44 46 files, dirs = [], []
45 47
46 48 try:
47 49 fullpath = join(absroot, root)
48 50 for name, kind in osutil.listdir(fullpath):
49 51 if kind == stat.S_IFDIR:
50 52 if name == '.hg':
51 53 if not reporoot:
52 54 return
53 55 else:
54 56 dirs.append(name)
55 57 path = join(root, name)
56 58 if dirstate._ignore(path):
57 59 continue
58 60 for result in walkit(path, False):
59 61 yield result
60 62 elif kind in (stat.S_IFREG, stat.S_IFLNK):
61 63 files.append(name)
62 64 yield fullpath, dirs, files
63 65
64 66 except OSError, err:
65 67 if err.errno == errno.ENOTDIR:
66 68 # fullpath was a directory, but has since been replaced
67 69 # by a file.
68 70 yield fullpath, dirs, files
69 71 elif err.errno not in walk_ignored_errors:
70 72 raise
71 73
72 74 return walkit(root, root == '')
73 75
74 76 class directory(object):
75 77 """
76 78 Representing a directory
77 79
78 80 * path is the relative path from repo root to this directory
79 81 * files is a dict listing the files in this directory
80 82 - keys are file names
81 83 - values are file status
82 84 * dirs is a dict listing the subdirectories
83 85 - key are subdirectories names
84 86 - values are directory objects
85 87 """
86 88 def __init__(self, relpath=''):
87 89 self.path = relpath
88 90 self.files = {}
89 91 self.dirs = {}
90 92
91 93 def dir(self, relpath):
92 94 """
93 95 Returns the directory contained at the relative path relpath.
94 96 Creates the intermediate directories if necessary.
95 97 """
96 98 if not relpath:
97 99 return self
98 100 l = relpath.split('/')
99 101 ret = self
100 102 while l:
101 103 next = l.pop(0)
102 104 try:
103 105 ret = ret.dirs[next]
104 106 except KeyError:
105 107 d = directory(join(ret.path, next))
106 108 ret.dirs[next] = d
107 109 ret = d
108 110 return ret
109 111
110 112 def walk(self, states, visited=None):
111 113 """
112 114 yield (filename, status) pairs for items in the trees
113 115 that have status in states.
114 116 filenames are relative to the repo root
115 117 """
116 118 for file, st in self.files.iteritems():
117 119 if st in states:
118 120 yield join(self.path, file), st
119 121 for dir in self.dirs.itervalues():
120 122 if visited is not None:
121 123 visited.add(dir.path)
122 124 for e in dir.walk(states):
123 125 yield e
124 126
125 127 def lookup(self, states, path, visited):
126 128 """
127 129 yield root-relative filenames that match path, and whose
128 130 status are in states:
129 131 * if path is a file, yield path
130 132 * if path is a directory, yield directory files
131 133 * if path is not tracked, yield nothing
132 134 """
133 135 if path[-1] == '/':
134 136 path = path[:-1]
135 137
136 138 paths = path.split('/')
137 139
138 140 # we need to check separately for last node
139 141 last = paths.pop()
140 142
141 143 tree = self
142 144 try:
143 145 for dir in paths:
144 146 tree = tree.dirs[dir]
145 147 except KeyError:
146 148 # path is not tracked
147 149 visited.add(tree.path)
148 150 return
149 151
150 152 try:
151 153 # if path is a directory, walk it
152 154 target = tree.dirs[last]
153 155 visited.add(target.path)
154 156 for file, st in target.walk(states, visited):
155 157 yield file
156 158 except KeyError:
157 159 try:
158 160 if tree.files[last] in states:
159 161 # path is a file
160 162 visited.add(tree.path)
161 163 yield path
162 164 except KeyError:
163 165 # path is not tracked
164 166 pass
165 167
166 168 class repowatcher(object):
167 169 """
168 170 Watches inotify events
169 171 """
170 172 statuskeys = 'almr!?'
171 173
172 174 def __init__(self, ui, dirstate, root):
173 175 self.ui = ui
174 176 self.dirstate = dirstate
175 177
176 178 self.wprefix = join(root, '')
177 179 self.prefixlen = len(self.wprefix)
178 180
179 181 self.tree = directory()
180 182 self.statcache = {}
181 183 self.statustrees = dict([(s, directory()) for s in self.statuskeys])
182 184
183 185 self.ds_info = self.dirstate_info()
184 186
185 187 self.last_event = None
186 188
187 189
188 190 def handle_timeout(self):
189 191 pass
190 192
191 193 def dirstate_info(self):
192 194 try:
193 195 st = os.lstat(self.wprefix + '.hg/dirstate')
194 196 return st.st_mtime, st.st_ino
195 197 except OSError, err:
196 198 if err.errno != errno.ENOENT:
197 199 raise
198 200 return 0, 0
199 201
200 202 def filestatus(self, fn, st):
201 203 try:
202 204 type_, mode, size, time = self.dirstate._map[fn][:4]
203 205 except KeyError:
204 206 type_ = '?'
205 207 if type_ == 'n':
206 208 st_mode, st_size, st_mtime = st
207 209 if size == -1:
208 210 return 'l'
209 211 if size and (size != st_size or (mode ^ st_mode) & 0100):
210 212 return 'm'
211 213 if time != int(st_mtime):
212 214 return 'l'
213 215 return 'n'
214 216 if type_ == '?' and self.dirstate._ignore(fn):
215 217 return 'i'
216 218 return type_
217 219
218 220 def updatefile(self, wfn, osstat):
219 221 '''
220 222 update the file entry of an existing file.
221 223
222 224 osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
223 225 '''
224 226
225 227 self._updatestatus(wfn, self.filestatus(wfn, osstat))
226 228
227 229 def deletefile(self, wfn, oldstatus):
228 230 '''
229 231 update the entry of a file which has been deleted.
230 232
231 233 oldstatus: char in statuskeys, status of the file before deletion
232 234 '''
233 235 if oldstatus == 'r':
234 236 newstatus = 'r'
235 237 elif oldstatus in 'almn':
236 238 newstatus = '!'
237 239 else:
238 240 newstatus = None
239 241
240 242 self.statcache.pop(wfn, None)
241 243 self._updatestatus(wfn, newstatus)
242 244
243 245 def _updatestatus(self, wfn, newstatus):
244 246 '''
245 247 Update the stored status of a file.
246 248
247 249 newstatus: - char in (statuskeys + 'ni'), new status to apply.
248 250 - or None, to stop tracking wfn
249 251 '''
250 252 root, fn = split(wfn)
251 253 d = self.tree.dir(root)
252 254
253 255 oldstatus = d.files.get(fn)
254 256 # oldstatus can be either:
255 257 # - None : fn is new
256 258 # - a char in statuskeys: fn is a (tracked) file
257 259
258 260 if self.ui.debugflag and oldstatus != newstatus:
259 261 self.ui.note(_('status: %r %s -> %s\n') %
260 262 (wfn, oldstatus, newstatus))
261 263
262 264 if oldstatus and oldstatus in self.statuskeys \
263 265 and oldstatus != newstatus:
264 266 del self.statustrees[oldstatus].dir(root).files[fn]
265 267
266 268 if newstatus in (None, 'i'):
267 269 d.files.pop(fn, None)
268 270 elif oldstatus != newstatus:
269 271 d.files[fn] = newstatus
270 272 if newstatus != 'n':
271 273 self.statustrees[newstatus].dir(root).files[fn] = newstatus
272 274
273 275 def check_deleted(self, key):
274 276 # Files that had been deleted but were present in the dirstate
275 277 # may have vanished from the dirstate; we must clean them up.
276 278 nuke = []
277 279 for wfn, ignore in self.statustrees[key].walk(key):
278 280 if wfn not in self.dirstate:
279 281 nuke.append(wfn)
280 282 for wfn in nuke:
281 283 root, fn = split(wfn)
282 284 del self.statustrees[key].dir(root).files[fn]
283 285 del self.tree.dir(root).files[fn]
284 286
285 287 def update_hgignore(self):
286 288 # An update of the ignore file can potentially change the
287 289 # states of all unknown and ignored files.
288 290
289 291 # XXX If the user has other ignore files outside the repo, or
290 292 # changes their list of ignore files at run time, we'll
291 293 # potentially never see changes to them. We could get the
292 294 # client to report to us what ignore data they're using.
293 295 # But it's easier to do nothing than to open that can of
294 296 # worms.
295 297
296 298 if '_ignore' in self.dirstate.__dict__:
297 299 delattr(self.dirstate, '_ignore')
298 300 self.ui.note(_('rescanning due to .hgignore change\n'))
299 301 self.handle_timeout()
300 302 self.scan()
301 303
302 304 def getstat(self, wpath):
303 305 try:
304 306 return self.statcache[wpath]
305 307 except KeyError:
306 308 try:
307 309 return self.stat(wpath)
308 310 except OSError, err:
309 311 if err.errno != errno.ENOENT:
310 312 raise
311 313
312 314 def stat(self, wpath):
313 315 try:
314 316 st = os.lstat(join(self.wprefix, wpath))
315 317 ret = st.st_mode, st.st_size, st.st_mtime
316 318 self.statcache[wpath] = ret
317 319 return ret
318 320 except OSError:
319 321 self.statcache.pop(wpath, None)
320 322 raise
321 323
322 324 class socketlistener(object):
323 325 """
324 326 Listens for client queries on unix socket inotify.sock
325 327 """
326 328 def __init__(self, ui, root, repowatcher, timeout):
327 329 self.ui = ui
328 330 self.repowatcher = repowatcher
329 331 self.sock = socket.socket(socket.AF_UNIX)
330 332 self.sockpath = join(root, '.hg/inotify.sock')
331 333 self.realsockpath = None
332 334 try:
333 335 self.sock.bind(self.sockpath)
334 336 except socket.error, err:
335 337 if err[0] == errno.EADDRINUSE:
336 338 raise AlreadyStartedException(_('cannot start: socket is '
337 339 'already bound'))
338 340 if err[0] == "AF_UNIX path too long":
339 341 if os.path.islink(self.sockpath) and \
340 342 not os.path.exists(self.sockpath):
341 343 raise util.Abort('inotify-server: cannot start: '
342 344 '.hg/inotify.sock is a broken symlink')
343 345 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
344 346 self.realsockpath = os.path.join(tempdir, "inotify.sock")
345 347 try:
346 348 self.sock.bind(self.realsockpath)
347 349 os.symlink(self.realsockpath, self.sockpath)
348 350 except (OSError, socket.error), inst:
349 351 try:
350 352 os.unlink(self.realsockpath)
351 353 except:
352 354 pass
353 355 os.rmdir(tempdir)
354 356 if inst.errno == errno.EEXIST:
355 357 raise AlreadyStartedException(_('cannot start: tried '
356 358 'linking .hg/inotify.sock to a temporary socket but'
357 359 ' .hg/inotify.sock already exists'))
358 360 raise
359 361 else:
360 362 raise
361 363 self.sock.listen(5)
362 364 self.fileno = self.sock.fileno
363 365
364 366 def answer_stat_query(self, cs):
365 367 names = cs.read().split('\0')
366 368
367 369 states = names.pop()
368 370
369 371 self.ui.note(_('answering query for %r\n') % states)
370 372
371 373 visited = set()
372 374 if not names:
373 375 def genresult(states, tree):
374 376 for fn, state in tree.walk(states):
375 377 yield fn
376 378 else:
377 379 def genresult(states, tree):
378 380 for fn in names:
379 381 for f in tree.lookup(states, fn, visited):
380 382 yield f
381 383
382 384 return ['\0'.join(r) for r in [
383 385 genresult('l', self.repowatcher.statustrees['l']),
384 386 genresult('m', self.repowatcher.statustrees['m']),
385 387 genresult('a', self.repowatcher.statustrees['a']),
386 388 genresult('r', self.repowatcher.statustrees['r']),
387 389 genresult('!', self.repowatcher.statustrees['!']),
388 390 '?' in states
389 391 and genresult('?', self.repowatcher.statustrees['?'])
390 392 or [],
391 393 [],
392 394 'c' in states and genresult('n', self.repowatcher.tree) or [],
393 395 visited
394 396 ]]
395 397
396 398 def answer_dbug_query(self):
397 399 return ['\0'.join(self.repowatcher.debug())]
398 400
399 401 def accept_connection(self):
400 402 sock, addr = self.sock.accept()
401 403
402 404 cs = common.recvcs(sock)
403 405 version = ord(cs.read(1))
404 406
405 407 if version != common.version:
406 408 self.ui.warn(_('received query from incompatible client '
407 409 'version %d\n') % version)
408 410 try:
409 411 # try to send back our version to the client
410 412 # this way, the client too is informed of the mismatch
411 413 sock.sendall(chr(common.version))
412 414 except:
413 415 pass
414 416 return
415 417
416 418 type = cs.read(4)
417 419
418 420 if type == 'STAT':
419 421 results = self.answer_stat_query(cs)
420 422 elif type == 'DBUG':
421 423 results = self.answer_dbug_query()
422 424 else:
423 425 self.ui.warn(_('unrecognized query type: %s\n') % type)
424 426 return
425 427
426 428 try:
427 429 try:
428 430 v = chr(common.version)
429 431
430 432 sock.sendall(v + type + struct.pack(common.resphdrfmts[type],
431 433 *map(len, results)))
432 434 sock.sendall(''.join(results))
433 435 finally:
434 436 sock.shutdown(socket.SHUT_WR)
435 437 except socket.error, err:
436 438 if err[0] != errno.EPIPE:
437 439 raise
438 440
439 441 if sys.platform == 'linux2':
440 442 import linuxserver as _server
441 443 else:
442 444 raise ImportError
443 445
444 446 master = _server.master
445 447
446 448 def start(ui, dirstate, root, opts):
447 timeout = opts.get('timeout')
449 timeout = opts.get('idle_timeout')
448 450 if timeout:
449 timeout = float(timeout) * 1e3
451 timeout = float(timeout) * 60000
452 else:
453 timeout = None
450 454
451 455 class service(object):
452 456 def init(self):
453 457 try:
454 458 self.master = master(ui, dirstate, root, timeout)
455 459 except AlreadyStartedException, inst:
456 460 raise util.Abort("inotify-server: %s" % inst)
457 461
458 462 def run(self):
459 463 try:
460 self.master.run()
464 try:
465 self.master.run()
466 except TimeoutException:
467 pass
461 468 finally:
462 469 self.master.shutdown()
463 470
464 471 if 'inserve' not in sys.argv:
465 472 runargs = util.hgcmd() + ['inserve', '-R', root]
466 473 else:
467 474 runargs = util.hgcmd() + sys.argv[1:]
468 475
469 476 pidfile = ui.config('inotify', 'pidfile')
470 477 if opts['daemon'] and pidfile is not None and 'pid-file' not in runargs:
471 478 runargs.append("--pid-file=%s" % pidfile)
472 479
473 480 service = service()
474 481 logfile = ui.config('inotify', 'log')
475 482
476 483 appendpid = ui.configbool('inotify', 'appendpid', False)
477 484
478 485 cmdutil.service(opts, initfn=service.init, runfn=service.run,
479 486 logfile=logfile, runargs=runargs, appendpid=appendpid)
General Comments 0
You need to be logged in to leave comments. Login now