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