##// END OF EJS Templates
Fixing issue1542, adding a relevant test...
Nicolas Dumazet -
r7892:67e59a98 default
parent child Browse files
Show More
@@ -0,0 +1,32 b''
1 #!/bin/sh
2
3 "$TESTDIR/hghave" inotify || exit 80
4
5 hg init
6
7 touch a
8 mkdir dir
9 touch dir/b
10 touch dir/c
11
12 echo "[extensions]" >> $HGRCPATH
13 echo "inotify=" >> $HGRCPATH
14
15 hg add dir/c
16
17 echo % inserve
18 hg inserve -d --pid-file=hg.pid 2>&1
19 cat hg.pid >> "$DAEMON_PIDS"
20
21 hg st
22
23 echo % moving dir out
24 mv dir ../tmp-test-inotify-issue1542
25
26 echo % status
27 hg st
28
29 sleep 1
30 echo "Are we able to kill the service? if not, the service died on some error"
31 kill `cat hg.pid`
32
@@ -0,0 +1,11 b''
1 % inserve
2 A dir/c
3 ? a
4 ? dir/b
5 ? hg.pid
6 % moving dir out
7 % status
8 ! dir/c
9 ? a
10 ? hg.pid
11 Are we able to kill the service? if not, the service died on some error
@@ -1,758 +1,762 b''
1 # server.py - inotify status server
1 # server.py - inotify status server
2 #
2 #
3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
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 common
11 import common
12 import errno, os, select, socket, stat, struct, sys, tempfile, time
12 import errno, os, select, socket, stat, struct, sys, tempfile, time
13
13
14 try:
14 try:
15 import linux as inotify
15 import linux as inotify
16 from linux import watcher
16 from linux import watcher
17 except ImportError:
17 except ImportError:
18 raise
18 raise
19
19
20 class AlreadyStartedException(Exception): pass
20 class AlreadyStartedException(Exception): pass
21
21
22 def join(a, b):
22 def join(a, b):
23 if a:
23 if a:
24 if a[-1] == '/':
24 if a[-1] == '/':
25 return a + b
25 return a + b
26 return a + '/' + b
26 return a + '/' + b
27 return b
27 return b
28
28
29 walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
29 walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
30
30
31 def walkrepodirs(repo):
31 def walkrepodirs(repo):
32 '''Iterate over all subdirectories of this repo.
32 '''Iterate over all subdirectories of this repo.
33 Exclude the .hg directory, any nested repos, and ignored dirs.'''
33 Exclude the .hg directory, any nested repos, and ignored dirs.'''
34 rootslash = repo.root + os.sep
34 rootslash = repo.root + os.sep
35 def walkit(dirname, top):
35 def walkit(dirname, top):
36 hginside = False
36 hginside = False
37 try:
37 try:
38 for name, kind in osutil.listdir(rootslash + dirname):
38 for name, kind in osutil.listdir(rootslash + dirname):
39 if kind == stat.S_IFDIR:
39 if kind == stat.S_IFDIR:
40 if name == '.hg':
40 if name == '.hg':
41 hginside = True
41 hginside = True
42 if not top: break
42 if not top: break
43 else:
43 else:
44 d = join(dirname, name)
44 d = join(dirname, name)
45 if repo.dirstate._ignore(d):
45 if repo.dirstate._ignore(d):
46 continue
46 continue
47 for subdir, hginsub in walkit(d, False):
47 for subdir, hginsub in walkit(d, False):
48 if not hginsub:
48 if not hginsub:
49 yield subdir, False
49 yield subdir, False
50 except OSError, err:
50 except OSError, err:
51 if err.errno not in walk_ignored_errors:
51 if err.errno not in walk_ignored_errors:
52 raise
52 raise
53 yield rootslash + dirname, hginside
53 yield rootslash + dirname, hginside
54 for dirname, hginside in walkit('', True):
54 for dirname, hginside in walkit('', True):
55 yield dirname
55 yield dirname
56
56
57 def walk(repo, root):
57 def walk(repo, root):
58 '''Like os.walk, but only yields regular files.'''
58 '''Like os.walk, but only yields regular files.'''
59
59
60 # This function is critical to performance during startup.
60 # This function is critical to performance during startup.
61
61
62 reporoot = root == ''
62 reporoot = root == ''
63 rootslash = repo.root + os.sep
63 rootslash = repo.root + os.sep
64
64
65 def walkit(root, reporoot):
65 def walkit(root, reporoot):
66 files, dirs = [], []
66 files, dirs = [], []
67 hginside = False
67 hginside = False
68
68
69 try:
69 try:
70 fullpath = rootslash + root
70 fullpath = rootslash + root
71 for name, kind in osutil.listdir(fullpath):
71 for name, kind in osutil.listdir(fullpath):
72 if kind == stat.S_IFDIR:
72 if kind == stat.S_IFDIR:
73 if name == '.hg':
73 if name == '.hg':
74 hginside = True
74 hginside = True
75 if reporoot:
75 if reporoot:
76 continue
76 continue
77 else:
77 else:
78 break
78 break
79 dirs.append(name)
79 dirs.append(name)
80 elif kind in (stat.S_IFREG, stat.S_IFLNK):
80 elif kind in (stat.S_IFREG, stat.S_IFLNK):
81 path = join(root, name)
81 path = join(root, name)
82 files.append((name, kind))
82 files.append((name, kind))
83
83
84 yield hginside, fullpath, dirs, files
84 yield hginside, fullpath, dirs, files
85
85
86 for subdir in dirs:
86 for subdir in dirs:
87 path = join(root, subdir)
87 path = join(root, subdir)
88 if repo.dirstate._ignore(path):
88 if repo.dirstate._ignore(path):
89 continue
89 continue
90 for result in walkit(path, False):
90 for result in walkit(path, False):
91 if not result[0]:
91 if not result[0]:
92 yield result
92 yield result
93 except OSError, err:
93 except OSError, err:
94 if err.errno not in walk_ignored_errors:
94 if err.errno not in walk_ignored_errors:
95 raise
95 raise
96 for result in walkit(root, reporoot):
96 for result in walkit(root, reporoot):
97 yield result[1:]
97 yield result[1:]
98
98
99 def _explain_watch_limit(ui, repo, count):
99 def _explain_watch_limit(ui, repo, count):
100 path = '/proc/sys/fs/inotify/max_user_watches'
100 path = '/proc/sys/fs/inotify/max_user_watches'
101 try:
101 try:
102 limit = int(file(path).read())
102 limit = int(file(path).read())
103 except IOError, err:
103 except IOError, err:
104 if err.errno != errno.ENOENT:
104 if err.errno != errno.ENOENT:
105 raise
105 raise
106 raise util.Abort(_('this system does not seem to '
106 raise util.Abort(_('this system does not seem to '
107 'support inotify'))
107 'support inotify'))
108 ui.warn(_('*** the current per-user limit on the number '
108 ui.warn(_('*** the current per-user limit on the number '
109 'of inotify watches is %s\n') % limit)
109 'of inotify watches is %s\n') % limit)
110 ui.warn(_('*** this limit is too low to watch every '
110 ui.warn(_('*** this limit is too low to watch every '
111 'directory in this repository\n'))
111 'directory in this repository\n'))
112 ui.warn(_('*** counting directories: '))
112 ui.warn(_('*** counting directories: '))
113 ndirs = len(list(walkrepodirs(repo)))
113 ndirs = len(list(walkrepodirs(repo)))
114 ui.warn(_('found %d\n') % ndirs)
114 ui.warn(_('found %d\n') % ndirs)
115 newlimit = min(limit, 1024)
115 newlimit = min(limit, 1024)
116 while newlimit < ((limit + ndirs) * 1.1):
116 while newlimit < ((limit + ndirs) * 1.1):
117 newlimit *= 2
117 newlimit *= 2
118 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
118 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
119 (limit, newlimit))
119 (limit, newlimit))
120 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
120 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
121 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
121 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
122 % repo.root)
122 % repo.root)
123
123
124 class Watcher(object):
124 class Watcher(object):
125 poll_events = select.POLLIN
125 poll_events = select.POLLIN
126 statuskeys = 'almr!?'
126 statuskeys = 'almr!?'
127
127
128 def __init__(self, ui, repo, master):
128 def __init__(self, ui, repo, master):
129 self.ui = ui
129 self.ui = ui
130 self.repo = repo
130 self.repo = repo
131 self.wprefix = self.repo.wjoin('')
131 self.wprefix = self.repo.wjoin('')
132 self.timeout = None
132 self.timeout = None
133 self.master = master
133 self.master = master
134 self.mask = (
134 self.mask = (
135 inotify.IN_ATTRIB |
135 inotify.IN_ATTRIB |
136 inotify.IN_CREATE |
136 inotify.IN_CREATE |
137 inotify.IN_DELETE |
137 inotify.IN_DELETE |
138 inotify.IN_DELETE_SELF |
138 inotify.IN_DELETE_SELF |
139 inotify.IN_MODIFY |
139 inotify.IN_MODIFY |
140 inotify.IN_MOVED_FROM |
140 inotify.IN_MOVED_FROM |
141 inotify.IN_MOVED_TO |
141 inotify.IN_MOVED_TO |
142 inotify.IN_MOVE_SELF |
142 inotify.IN_MOVE_SELF |
143 inotify.IN_ONLYDIR |
143 inotify.IN_ONLYDIR |
144 inotify.IN_UNMOUNT |
144 inotify.IN_UNMOUNT |
145 0)
145 0)
146 try:
146 try:
147 self.watcher = watcher.Watcher()
147 self.watcher = watcher.Watcher()
148 except OSError, err:
148 except OSError, err:
149 raise util.Abort(_('inotify service not available: %s') %
149 raise util.Abort(_('inotify service not available: %s') %
150 err.strerror)
150 err.strerror)
151 self.threshold = watcher.Threshold(self.watcher)
151 self.threshold = watcher.Threshold(self.watcher)
152 self.registered = True
152 self.registered = True
153 self.fileno = self.watcher.fileno
153 self.fileno = self.watcher.fileno
154
154
155 self.repo.dirstate.__class__.inotifyserver = True
155 self.repo.dirstate.__class__.inotifyserver = True
156
156
157 self.tree = {}
157 self.tree = {}
158 self.statcache = {}
158 self.statcache = {}
159 self.statustrees = dict([(s, {}) for s in self.statuskeys])
159 self.statustrees = dict([(s, {}) for s in self.statuskeys])
160
160
161 self.watches = 0
161 self.watches = 0
162 self.last_event = None
162 self.last_event = None
163
163
164 self.eventq = {}
164 self.eventq = {}
165 self.deferred = 0
165 self.deferred = 0
166
166
167 self.ds_info = self.dirstate_info()
167 self.ds_info = self.dirstate_info()
168 self.scan()
168 self.scan()
169
169
170 def event_time(self):
170 def event_time(self):
171 last = self.last_event
171 last = self.last_event
172 now = time.time()
172 now = time.time()
173 self.last_event = now
173 self.last_event = now
174
174
175 if last is None:
175 if last is None:
176 return 'start'
176 return 'start'
177 delta = now - last
177 delta = now - last
178 if delta < 5:
178 if delta < 5:
179 return '+%.3f' % delta
179 return '+%.3f' % delta
180 if delta < 50:
180 if delta < 50:
181 return '+%.2f' % delta
181 return '+%.2f' % delta
182 return '+%.1f' % delta
182 return '+%.1f' % delta
183
183
184 def dirstate_info(self):
184 def dirstate_info(self):
185 try:
185 try:
186 st = os.lstat(self.repo.join('dirstate'))
186 st = os.lstat(self.repo.join('dirstate'))
187 return st.st_mtime, st.st_ino
187 return st.st_mtime, st.st_ino
188 except OSError, err:
188 except OSError, err:
189 if err.errno != errno.ENOENT:
189 if err.errno != errno.ENOENT:
190 raise
190 raise
191 return 0, 0
191 return 0, 0
192
192
193 def add_watch(self, path, mask):
193 def add_watch(self, path, mask):
194 if not path:
194 if not path:
195 return
195 return
196 if self.watcher.path(path) is None:
196 if self.watcher.path(path) is None:
197 if self.ui.debugflag:
197 if self.ui.debugflag:
198 self.ui.note(_('watching %r\n') % path[len(self.wprefix):])
198 self.ui.note(_('watching %r\n') % path[len(self.wprefix):])
199 try:
199 try:
200 self.watcher.add(path, mask)
200 self.watcher.add(path, mask)
201 self.watches += 1
201 self.watches += 1
202 except OSError, err:
202 except OSError, err:
203 if err.errno in (errno.ENOENT, errno.ENOTDIR):
203 if err.errno in (errno.ENOENT, errno.ENOTDIR):
204 return
204 return
205 if err.errno != errno.ENOSPC:
205 if err.errno != errno.ENOSPC:
206 raise
206 raise
207 _explain_watch_limit(self.ui, self.repo, self.watches)
207 _explain_watch_limit(self.ui, self.repo, self.watches)
208
208
209 def setup(self):
209 def setup(self):
210 self.ui.note(_('watching directories under %r\n') % self.repo.root)
210 self.ui.note(_('watching directories under %r\n') % self.repo.root)
211 self.add_watch(self.repo.path, inotify.IN_DELETE)
211 self.add_watch(self.repo.path, inotify.IN_DELETE)
212 self.check_dirstate()
212 self.check_dirstate()
213
213
214 def wpath(self, evt):
214 def wpath(self, evt):
215 path = evt.fullpath
215 path = evt.fullpath
216 if path == self.repo.root:
216 if path == self.repo.root:
217 return ''
217 return ''
218 if path.startswith(self.wprefix):
218 if path.startswith(self.wprefix):
219 return path[len(self.wprefix):]
219 return path[len(self.wprefix):]
220 raise 'wtf? ' + path
220 raise 'wtf? ' + path
221
221
222 def dir(self, tree, path):
222 def dir(self, tree, path):
223 if path:
223 if path:
224 for name in path.split('/'):
224 for name in path.split('/'):
225 tree.setdefault(name, {})
225 tree.setdefault(name, {})
226 tree = tree[name]
226 tree = tree[name]
227 return tree
227 return tree
228
228
229 def lookup(self, path, tree):
229 def lookup(self, path, tree):
230 if path:
230 if path:
231 try:
231 try:
232 for name in path.split('/'):
232 for name in path.split('/'):
233 tree = tree[name]
233 tree = tree[name]
234 except KeyError:
234 except KeyError:
235 return 'x'
235 return 'x'
236 except TypeError:
236 except TypeError:
237 return 'd'
237 return 'd'
238 return tree
238 return tree
239
239
240 def split(self, path):
240 def split(self, path):
241 c = path.rfind('/')
241 c = path.rfind('/')
242 if c == -1:
242 if c == -1:
243 return '', path
243 return '', path
244 return path[:c], path[c+1:]
244 return path[:c], path[c+1:]
245
245
246 def filestatus(self, fn, st):
246 def filestatus(self, fn, st):
247 try:
247 try:
248 type_, mode, size, time = self.repo.dirstate._map[fn][:4]
248 type_, mode, size, time = self.repo.dirstate._map[fn][:4]
249 except KeyError:
249 except KeyError:
250 type_ = '?'
250 type_ = '?'
251 if type_ == 'n':
251 if type_ == 'n':
252 if not st:
252 if not st:
253 return '!'
253 return '!'
254 st_mode, st_size, st_mtime = st
254 st_mode, st_size, st_mtime = st
255 if size == -1:
255 if size == -1:
256 return 'l'
256 return 'l'
257 if size and (size != st_size or (mode ^ st_mode) & 0100):
257 if size and (size != st_size or (mode ^ st_mode) & 0100):
258 return 'm'
258 return 'm'
259 if time != int(st_mtime):
259 if time != int(st_mtime):
260 return 'l'
260 return 'l'
261 return 'n'
261 return 'n'
262 if type_ in 'ma' and not st:
262 if type_ in 'ma' and not st:
263 return '!'
263 return '!'
264 if type_ == '?' and self.repo.dirstate._ignore(fn):
264 if type_ == '?' and self.repo.dirstate._ignore(fn):
265 return 'i'
265 return 'i'
266 return type_
266 return type_
267
267
268 def updatestatus(self, wfn, st=None, status=None):
268 def updatestatus(self, wfn, st=None, status=None):
269 if st:
269 if st:
270 status = self.filestatus(wfn, st)
270 status = self.filestatus(wfn, st)
271 else:
271 else:
272 self.statcache.pop(wfn, None)
272 self.statcache.pop(wfn, None)
273 root, fn = self.split(wfn)
273 root, fn = self.split(wfn)
274 d = self.dir(self.tree, root)
274 d = self.dir(self.tree, root)
275 oldstatus = d.get(fn)
275 oldstatus = d.get(fn)
276 isdir = False
276 isdir = False
277 if oldstatus:
277 if oldstatus:
278 try:
278 try:
279 if not status:
279 if not status:
280 if oldstatus in 'almn':
280 if oldstatus in 'almn':
281 status = '!'
281 status = '!'
282 elif oldstatus == 'r':
282 elif oldstatus == 'r':
283 status = 'r'
283 status = 'r'
284 except TypeError:
284 except TypeError:
285 # oldstatus may be a dict left behind by a deleted
285 # oldstatus may be a dict left behind by a deleted
286 # directory
286 # directory
287 isdir = True
287 isdir = True
288 else:
288 else:
289 if oldstatus in self.statuskeys and oldstatus != status:
289 if oldstatus in self.statuskeys and oldstatus != status:
290 del self.dir(self.statustrees[oldstatus], root)[fn]
290 del self.dir(self.statustrees[oldstatus], root)[fn]
291 if self.ui.debugflag and oldstatus != status:
291 if self.ui.debugflag and oldstatus != status:
292 if isdir:
292 if isdir:
293 self.ui.note(_('status: %r dir(%d) -> %s\n') %
293 self.ui.note(_('status: %r dir(%d) -> %s\n') %
294 (wfn, len(oldstatus), status))
294 (wfn, len(oldstatus), status))
295 else:
295 else:
296 self.ui.note(_('status: %r %s -> %s\n') %
296 self.ui.note(_('status: %r %s -> %s\n') %
297 (wfn, oldstatus, status))
297 (wfn, oldstatus, status))
298 if not isdir:
298 if not isdir:
299 if status and status != 'i':
299 if status and status != 'i':
300 d[fn] = status
300 d[fn] = status
301 if status in self.statuskeys:
301 if status in self.statuskeys:
302 dd = self.dir(self.statustrees[status], root)
302 dd = self.dir(self.statustrees[status], root)
303 if oldstatus != status or fn not in dd:
303 if oldstatus != status or fn not in dd:
304 dd[fn] = status
304 dd[fn] = status
305 else:
305 else:
306 d.pop(fn, None)
306 d.pop(fn, None)
307 elif not status:
308 # a directory is being removed, check its contents
309 for subfile, b in oldstatus.copy().iteritems():
310 self.updatestatus(wfn + '/' + subfile, None)
311
307
312
308 def check_deleted(self, key):
313 def check_deleted(self, key):
309 # Files that had been deleted but were present in the dirstate
314 # Files that had been deleted but were present in the dirstate
310 # may have vanished from the dirstate; we must clean them up.
315 # may have vanished from the dirstate; we must clean them up.
311 nuke = []
316 nuke = []
312 for wfn, ignore in self.walk(key, self.statustrees[key]):
317 for wfn, ignore in self.walk(key, self.statustrees[key]):
313 if wfn not in self.repo.dirstate:
318 if wfn not in self.repo.dirstate:
314 nuke.append(wfn)
319 nuke.append(wfn)
315 for wfn in nuke:
320 for wfn in nuke:
316 root, fn = self.split(wfn)
321 root, fn = self.split(wfn)
317 del self.dir(self.statustrees[key], root)[fn]
322 del self.dir(self.statustrees[key], root)[fn]
318 del self.dir(self.tree, root)[fn]
323 del self.dir(self.tree, root)[fn]
319
324
320 def scan(self, topdir=''):
325 def scan(self, topdir=''):
321 self.handle_timeout()
326 self.handle_timeout()
322 ds = self.repo.dirstate._map.copy()
327 ds = self.repo.dirstate._map.copy()
323 self.add_watch(join(self.repo.root, topdir), self.mask)
328 self.add_watch(join(self.repo.root, topdir), self.mask)
324 for root, dirs, entries in walk(self.repo, topdir):
329 for root, dirs, entries in walk(self.repo, topdir):
325 for d in dirs:
330 for d in dirs:
326 self.add_watch(join(root, d), self.mask)
331 self.add_watch(join(root, d), self.mask)
327 wroot = root[len(self.wprefix):]
332 wroot = root[len(self.wprefix):]
328 d = self.dir(self.tree, wroot)
333 d = self.dir(self.tree, wroot)
329 for fn, kind in entries:
334 for fn, kind in entries:
330 wfn = join(wroot, fn)
335 wfn = join(wroot, fn)
331 self.updatestatus(wfn, self.getstat(wfn))
336 self.updatestatus(wfn, self.getstat(wfn))
332 ds.pop(wfn, None)
337 ds.pop(wfn, None)
333 wtopdir = topdir
338 wtopdir = topdir
334 if wtopdir and wtopdir[-1] != '/':
339 if wtopdir and wtopdir[-1] != '/':
335 wtopdir += '/'
340 wtopdir += '/'
336 for wfn, state in ds.iteritems():
341 for wfn, state in ds.iteritems():
337 if not wfn.startswith(wtopdir):
342 if not wfn.startswith(wtopdir):
338 continue
343 continue
339 try:
344 try:
340 st = self.stat(wfn)
345 st = self.stat(wfn)
341 except OSError:
346 except OSError:
342 status = state[0]
347 status = state[0]
343 self.updatestatus(wfn, None, status=status)
348 self.updatestatus(wfn, None, status=status)
344 else:
349 else:
345 self.updatestatus(wfn, st)
350 self.updatestatus(wfn, st)
346 self.check_deleted('!')
351 self.check_deleted('!')
347 self.check_deleted('r')
352 self.check_deleted('r')
348
353
349 def check_dirstate(self):
354 def check_dirstate(self):
350 ds_info = self.dirstate_info()
355 ds_info = self.dirstate_info()
351 if ds_info == self.ds_info:
356 if ds_info == self.ds_info:
352 return
357 return
353 self.ds_info = ds_info
358 self.ds_info = ds_info
354 if not self.ui.debugflag:
359 if not self.ui.debugflag:
355 self.last_event = None
360 self.last_event = None
356 self.ui.note(_('%s dirstate reload\n') % self.event_time())
361 self.ui.note(_('%s dirstate reload\n') % self.event_time())
357 self.repo.dirstate.invalidate()
362 self.repo.dirstate.invalidate()
358 self.scan()
363 self.scan()
359 self.ui.note(_('%s end dirstate reload\n') % self.event_time())
364 self.ui.note(_('%s end dirstate reload\n') % self.event_time())
360
365
361 def walk(self, states, tree, prefix=''):
366 def walk(self, states, tree, prefix=''):
362 # This is the "inner loop" when talking to the client.
367 # This is the "inner loop" when talking to the client.
363
368
364 for name, val in tree.iteritems():
369 for name, val in tree.iteritems():
365 path = join(prefix, name)
370 path = join(prefix, name)
366 try:
371 try:
367 if val in states:
372 if val in states:
368 yield path, val
373 yield path, val
369 except TypeError:
374 except TypeError:
370 for p in self.walk(states, val, path):
375 for p in self.walk(states, val, path):
371 yield p
376 yield p
372
377
373 def update_hgignore(self):
378 def update_hgignore(self):
374 # An update of the ignore file can potentially change the
379 # An update of the ignore file can potentially change the
375 # states of all unknown and ignored files.
380 # states of all unknown and ignored files.
376
381
377 # XXX If the user has other ignore files outside the repo, or
382 # XXX If the user has other ignore files outside the repo, or
378 # changes their list of ignore files at run time, we'll
383 # changes their list of ignore files at run time, we'll
379 # potentially never see changes to them. We could get the
384 # potentially never see changes to them. We could get the
380 # client to report to us what ignore data they're using.
385 # client to report to us what ignore data they're using.
381 # But it's easier to do nothing than to open that can of
386 # But it's easier to do nothing than to open that can of
382 # worms.
387 # worms.
383
388
384 if '_ignore' in self.repo.dirstate.__dict__:
389 if '_ignore' in self.repo.dirstate.__dict__:
385 delattr(self.repo.dirstate, '_ignore')
390 delattr(self.repo.dirstate, '_ignore')
386 self.ui.note(_('rescanning due to .hgignore change\n'))
391 self.ui.note(_('rescanning due to .hgignore change\n'))
387 self.scan()
392 self.scan()
388
393
389 def getstat(self, wpath):
394 def getstat(self, wpath):
390 try:
395 try:
391 return self.statcache[wpath]
396 return self.statcache[wpath]
392 except KeyError:
397 except KeyError:
393 try:
398 try:
394 return self.stat(wpath)
399 return self.stat(wpath)
395 except OSError, err:
400 except OSError, err:
396 if err.errno != errno.ENOENT:
401 if err.errno != errno.ENOENT:
397 raise
402 raise
398
403
399 def stat(self, wpath):
404 def stat(self, wpath):
400 try:
405 try:
401 st = os.lstat(join(self.wprefix, wpath))
406 st = os.lstat(join(self.wprefix, wpath))
402 ret = st.st_mode, st.st_size, st.st_mtime
407 ret = st.st_mode, st.st_size, st.st_mtime
403 self.statcache[wpath] = ret
408 self.statcache[wpath] = ret
404 return ret
409 return ret
405 except OSError:
410 except OSError:
406 self.statcache.pop(wpath, None)
411 self.statcache.pop(wpath, None)
407 raise
412 raise
408
413
409 def created(self, wpath):
414 def created(self, wpath):
410 if wpath == '.hgignore':
415 if wpath == '.hgignore':
411 self.update_hgignore()
416 self.update_hgignore()
412 try:
417 try:
413 st = self.stat(wpath)
418 st = self.stat(wpath)
414 if stat.S_ISREG(st[0]):
419 if stat.S_ISREG(st[0]):
415 self.updatestatus(wpath, st)
420 self.updatestatus(wpath, st)
416 except OSError:
421 except OSError:
417 pass
422 pass
418
423
419 def modified(self, wpath):
424 def modified(self, wpath):
420 if wpath == '.hgignore':
425 if wpath == '.hgignore':
421 self.update_hgignore()
426 self.update_hgignore()
422 try:
427 try:
423 st = self.stat(wpath)
428 st = self.stat(wpath)
424 if stat.S_ISREG(st[0]):
429 if stat.S_ISREG(st[0]):
425 if self.repo.dirstate[wpath] in 'lmn':
430 if self.repo.dirstate[wpath] in 'lmn':
426 self.updatestatus(wpath, st)
431 self.updatestatus(wpath, st)
427 except OSError:
432 except OSError:
428 pass
433 pass
429
434
430 def deleted(self, wpath):
435 def deleted(self, wpath):
431 if wpath == '.hgignore':
436 if wpath == '.hgignore':
432 self.update_hgignore()
437 self.update_hgignore()
433 elif wpath.startswith('.hg/'):
438 elif wpath.startswith('.hg/'):
434 if wpath == '.hg/wlock':
439 if wpath == '.hg/wlock':
435 self.check_dirstate()
440 self.check_dirstate()
436 return
441 return
437
442
438 self.updatestatus(wpath, None)
443 self.updatestatus(wpath, None)
439
444
440 def schedule_work(self, wpath, evt):
445 def schedule_work(self, wpath, evt):
441 self.eventq.setdefault(wpath, [])
446 self.eventq.setdefault(wpath, [])
442 prev = self.eventq[wpath]
447 prev = self.eventq[wpath]
443 try:
448 try:
444 if prev and evt == 'm' and prev[-1] in 'cm':
449 if prev and evt == 'm' and prev[-1] in 'cm':
445 return
450 return
446 self.eventq[wpath].append(evt)
451 self.eventq[wpath].append(evt)
447 finally:
452 finally:
448 self.deferred += 1
453 self.deferred += 1
449 self.timeout = 250
454 self.timeout = 250
450
455
451 def deferred_event(self, wpath, evt):
456 def deferred_event(self, wpath, evt):
452 if evt == 'c':
457 if evt == 'c':
453 self.created(wpath)
458 self.created(wpath)
454 elif evt == 'm':
459 elif evt == 'm':
455 self.modified(wpath)
460 self.modified(wpath)
456 elif evt == 'd':
461 elif evt == 'd':
457 self.deleted(wpath)
462 self.deleted(wpath)
458
463
459 def process_create(self, wpath, evt):
464 def process_create(self, wpath, evt):
460 if self.ui.debugflag:
465 if self.ui.debugflag:
461 self.ui.note(_('%s event: created %s\n') %
466 self.ui.note(_('%s event: created %s\n') %
462 (self.event_time(), wpath))
467 (self.event_time(), wpath))
463
468
464 if evt.mask & inotify.IN_ISDIR:
469 if evt.mask & inotify.IN_ISDIR:
465 self.scan(wpath)
470 self.scan(wpath)
466 else:
471 else:
467 self.schedule_work(wpath, 'c')
472 self.schedule_work(wpath, 'c')
468
473
469 def process_delete(self, wpath, evt):
474 def process_delete(self, wpath, evt):
470 if self.ui.debugflag:
475 if self.ui.debugflag:
471 self.ui.note(_('%s event: deleted %s\n') %
476 self.ui.note(_('%s event: deleted %s\n') %
472 (self.event_time(), wpath))
477 (self.event_time(), wpath))
473
478
474 if evt.mask & inotify.IN_ISDIR:
479 if evt.mask & inotify.IN_ISDIR:
475 self.scan(wpath)
480 self.scan(wpath)
476 else:
481 self.schedule_work(wpath, 'd')
477 self.schedule_work(wpath, 'd')
478
482
479 def process_modify(self, wpath, evt):
483 def process_modify(self, wpath, evt):
480 if self.ui.debugflag:
484 if self.ui.debugflag:
481 self.ui.note(_('%s event: modified %s\n') %
485 self.ui.note(_('%s event: modified %s\n') %
482 (self.event_time(), wpath))
486 (self.event_time(), wpath))
483
487
484 if not (evt.mask & inotify.IN_ISDIR):
488 if not (evt.mask & inotify.IN_ISDIR):
485 self.schedule_work(wpath, 'm')
489 self.schedule_work(wpath, 'm')
486
490
487 def process_unmount(self, evt):
491 def process_unmount(self, evt):
488 self.ui.warn(_('filesystem containing %s was unmounted\n') %
492 self.ui.warn(_('filesystem containing %s was unmounted\n') %
489 evt.fullpath)
493 evt.fullpath)
490 sys.exit(0)
494 sys.exit(0)
491
495
492 def handle_event(self, fd, event):
496 def handle_event(self, fd, event):
493 if self.ui.debugflag:
497 if self.ui.debugflag:
494 self.ui.note(_('%s readable: %d bytes\n') %
498 self.ui.note(_('%s readable: %d bytes\n') %
495 (self.event_time(), self.threshold.readable()))
499 (self.event_time(), self.threshold.readable()))
496 if not self.threshold():
500 if not self.threshold():
497 if self.registered:
501 if self.registered:
498 if self.ui.debugflag:
502 if self.ui.debugflag:
499 self.ui.note(_('%s below threshold - unhooking\n') %
503 self.ui.note(_('%s below threshold - unhooking\n') %
500 (self.event_time()))
504 (self.event_time()))
501 self.master.poll.unregister(fd)
505 self.master.poll.unregister(fd)
502 self.registered = False
506 self.registered = False
503 self.timeout = 250
507 self.timeout = 250
504 else:
508 else:
505 self.read_events()
509 self.read_events()
506
510
507 def read_events(self, bufsize=None):
511 def read_events(self, bufsize=None):
508 events = self.watcher.read(bufsize)
512 events = self.watcher.read(bufsize)
509 if self.ui.debugflag:
513 if self.ui.debugflag:
510 self.ui.note(_('%s reading %d events\n') %
514 self.ui.note(_('%s reading %d events\n') %
511 (self.event_time(), len(events)))
515 (self.event_time(), len(events)))
512 for evt in events:
516 for evt in events:
513 wpath = self.wpath(evt)
517 wpath = self.wpath(evt)
514 if evt.mask & inotify.IN_UNMOUNT:
518 if evt.mask & inotify.IN_UNMOUNT:
515 self.process_unmount(wpath, evt)
519 self.process_unmount(wpath, evt)
516 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
520 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
517 self.process_modify(wpath, evt)
521 self.process_modify(wpath, evt)
518 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
522 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
519 inotify.IN_MOVED_FROM):
523 inotify.IN_MOVED_FROM):
520 self.process_delete(wpath, evt)
524 self.process_delete(wpath, evt)
521 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
525 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
522 self.process_create(wpath, evt)
526 self.process_create(wpath, evt)
523
527
524 def handle_timeout(self):
528 def handle_timeout(self):
525 if not self.registered:
529 if not self.registered:
526 if self.ui.debugflag:
530 if self.ui.debugflag:
527 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
531 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
528 (self.event_time(), self.threshold.readable()))
532 (self.event_time(), self.threshold.readable()))
529 self.read_events(0)
533 self.read_events(0)
530 self.master.poll.register(self, select.POLLIN)
534 self.master.poll.register(self, select.POLLIN)
531 self.registered = True
535 self.registered = True
532
536
533 if self.eventq:
537 if self.eventq:
534 if self.ui.debugflag:
538 if self.ui.debugflag:
535 self.ui.note(_('%s processing %d deferred events as %d\n') %
539 self.ui.note(_('%s processing %d deferred events as %d\n') %
536 (self.event_time(), self.deferred,
540 (self.event_time(), self.deferred,
537 len(self.eventq)))
541 len(self.eventq)))
538 for wpath, evts in util.sort(self.eventq.items()):
542 for wpath, evts in util.sort(self.eventq.items()):
539 for evt in evts:
543 for evt in evts:
540 self.deferred_event(wpath, evt)
544 self.deferred_event(wpath, evt)
541 self.eventq.clear()
545 self.eventq.clear()
542 self.deferred = 0
546 self.deferred = 0
543 self.timeout = None
547 self.timeout = None
544
548
545 def shutdown(self):
549 def shutdown(self):
546 self.watcher.close()
550 self.watcher.close()
547
551
548 class Server(object):
552 class Server(object):
549 poll_events = select.POLLIN
553 poll_events = select.POLLIN
550
554
551 def __init__(self, ui, repo, watcher, timeout):
555 def __init__(self, ui, repo, watcher, timeout):
552 self.ui = ui
556 self.ui = ui
553 self.repo = repo
557 self.repo = repo
554 self.watcher = watcher
558 self.watcher = watcher
555 self.timeout = timeout
559 self.timeout = timeout
556 self.sock = socket.socket(socket.AF_UNIX)
560 self.sock = socket.socket(socket.AF_UNIX)
557 self.sockpath = self.repo.join('inotify.sock')
561 self.sockpath = self.repo.join('inotify.sock')
558 self.realsockpath = None
562 self.realsockpath = None
559 try:
563 try:
560 self.sock.bind(self.sockpath)
564 self.sock.bind(self.sockpath)
561 except socket.error, err:
565 except socket.error, err:
562 if err[0] == errno.EADDRINUSE:
566 if err[0] == errno.EADDRINUSE:
563 raise AlreadyStartedException(_('could not start server: %s')
567 raise AlreadyStartedException(_('could not start server: %s')
564 % err[1])
568 % err[1])
565 if err[0] == "AF_UNIX path too long":
569 if err[0] == "AF_UNIX path too long":
566 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
570 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
567 self.realsockpath = os.path.join(tempdir, "inotify.sock")
571 self.realsockpath = os.path.join(tempdir, "inotify.sock")
568 try:
572 try:
569 self.sock.bind(self.realsockpath)
573 self.sock.bind(self.realsockpath)
570 os.symlink(self.realsockpath, self.sockpath)
574 os.symlink(self.realsockpath, self.sockpath)
571 except (OSError, socket.error), inst:
575 except (OSError, socket.error), inst:
572 try:
576 try:
573 os.unlink(self.realsockpath)
577 os.unlink(self.realsockpath)
574 except:
578 except:
575 pass
579 pass
576 os.rmdir(tempdir)
580 os.rmdir(tempdir)
577 if inst.errno == errno.EEXIST:
581 if inst.errno == errno.EEXIST:
578 raise AlreadyStartedException(_('could not start server: %s')
582 raise AlreadyStartedException(_('could not start server: %s')
579 % inst.strerror)
583 % inst.strerror)
580 raise
584 raise
581 else:
585 else:
582 raise
586 raise
583 self.sock.listen(5)
587 self.sock.listen(5)
584 self.fileno = self.sock.fileno
588 self.fileno = self.sock.fileno
585
589
586 def handle_timeout(self):
590 def handle_timeout(self):
587 pass
591 pass
588
592
589 def handle_event(self, fd, event):
593 def handle_event(self, fd, event):
590 sock, addr = self.sock.accept()
594 sock, addr = self.sock.accept()
591
595
592 cs = common.recvcs(sock)
596 cs = common.recvcs(sock)
593 version = ord(cs.read(1))
597 version = ord(cs.read(1))
594
598
595 sock.sendall(chr(common.version))
599 sock.sendall(chr(common.version))
596
600
597 if version != common.version:
601 if version != common.version:
598 self.ui.warn(_('received query from incompatible client '
602 self.ui.warn(_('received query from incompatible client '
599 'version %d\n') % version)
603 'version %d\n') % version)
600 return
604 return
601
605
602 names = cs.read().split('\0')
606 names = cs.read().split('\0')
603
607
604 states = names.pop()
608 states = names.pop()
605
609
606 self.ui.note(_('answering query for %r\n') % states)
610 self.ui.note(_('answering query for %r\n') % states)
607
611
608 if self.watcher.timeout:
612 if self.watcher.timeout:
609 # We got a query while a rescan is pending. Make sure we
613 # We got a query while a rescan is pending. Make sure we
610 # rescan before responding, or we could give back a wrong
614 # rescan before responding, or we could give back a wrong
611 # answer.
615 # answer.
612 self.watcher.handle_timeout()
616 self.watcher.handle_timeout()
613
617
614 if not names:
618 if not names:
615 def genresult(states, tree):
619 def genresult(states, tree):
616 for fn, state in self.watcher.walk(states, tree):
620 for fn, state in self.watcher.walk(states, tree):
617 yield fn
621 yield fn
618 else:
622 else:
619 def genresult(states, tree):
623 def genresult(states, tree):
620 for fn in names:
624 for fn in names:
621 l = self.watcher.lookup(fn, tree)
625 l = self.watcher.lookup(fn, tree)
622 try:
626 try:
623 if l in states:
627 if l in states:
624 yield fn
628 yield fn
625 except TypeError:
629 except TypeError:
626 for f, s in self.watcher.walk(states, l, fn):
630 for f, s in self.watcher.walk(states, l, fn):
627 yield f
631 yield f
628
632
629 results = ['\0'.join(r) for r in [
633 results = ['\0'.join(r) for r in [
630 genresult('l', self.watcher.statustrees['l']),
634 genresult('l', self.watcher.statustrees['l']),
631 genresult('m', self.watcher.statustrees['m']),
635 genresult('m', self.watcher.statustrees['m']),
632 genresult('a', self.watcher.statustrees['a']),
636 genresult('a', self.watcher.statustrees['a']),
633 genresult('r', self.watcher.statustrees['r']),
637 genresult('r', self.watcher.statustrees['r']),
634 genresult('!', self.watcher.statustrees['!']),
638 genresult('!', self.watcher.statustrees['!']),
635 '?' in states and genresult('?', self.watcher.statustrees['?']) or [],
639 '?' in states and genresult('?', self.watcher.statustrees['?']) or [],
636 [],
640 [],
637 'c' in states and genresult('n', self.watcher.tree) or [],
641 'c' in states and genresult('n', self.watcher.tree) or [],
638 ]]
642 ]]
639
643
640 try:
644 try:
641 try:
645 try:
642 sock.sendall(struct.pack(common.resphdrfmt,
646 sock.sendall(struct.pack(common.resphdrfmt,
643 *map(len, results)))
647 *map(len, results)))
644 sock.sendall(''.join(results))
648 sock.sendall(''.join(results))
645 finally:
649 finally:
646 sock.shutdown(socket.SHUT_WR)
650 sock.shutdown(socket.SHUT_WR)
647 except socket.error, err:
651 except socket.error, err:
648 if err[0] != errno.EPIPE:
652 if err[0] != errno.EPIPE:
649 raise
653 raise
650
654
651 def shutdown(self):
655 def shutdown(self):
652 self.sock.close()
656 self.sock.close()
653 try:
657 try:
654 os.unlink(self.sockpath)
658 os.unlink(self.sockpath)
655 if self.realsockpath:
659 if self.realsockpath:
656 os.unlink(self.realsockpath)
660 os.unlink(self.realsockpath)
657 os.rmdir(os.path.dirname(self.realsockpath))
661 os.rmdir(os.path.dirname(self.realsockpath))
658 except OSError, err:
662 except OSError, err:
659 if err.errno != errno.ENOENT:
663 if err.errno != errno.ENOENT:
660 raise
664 raise
661
665
662 class Master(object):
666 class Master(object):
663 def __init__(self, ui, repo, timeout=None):
667 def __init__(self, ui, repo, timeout=None):
664 self.ui = ui
668 self.ui = ui
665 self.repo = repo
669 self.repo = repo
666 self.poll = select.poll()
670 self.poll = select.poll()
667 self.watcher = Watcher(ui, repo, self)
671 self.watcher = Watcher(ui, repo, self)
668 self.server = Server(ui, repo, self.watcher, timeout)
672 self.server = Server(ui, repo, self.watcher, timeout)
669 self.table = {}
673 self.table = {}
670 for obj in (self.watcher, self.server):
674 for obj in (self.watcher, self.server):
671 fd = obj.fileno()
675 fd = obj.fileno()
672 self.table[fd] = obj
676 self.table[fd] = obj
673 self.poll.register(fd, obj.poll_events)
677 self.poll.register(fd, obj.poll_events)
674
678
675 def register(self, fd, mask):
679 def register(self, fd, mask):
676 self.poll.register(fd, mask)
680 self.poll.register(fd, mask)
677
681
678 def shutdown(self):
682 def shutdown(self):
679 for obj in self.table.itervalues():
683 for obj in self.table.itervalues():
680 obj.shutdown()
684 obj.shutdown()
681
685
682 def run(self):
686 def run(self):
683 self.watcher.setup()
687 self.watcher.setup()
684 self.ui.note(_('finished setup\n'))
688 self.ui.note(_('finished setup\n'))
685 if os.getenv('TIME_STARTUP'):
689 if os.getenv('TIME_STARTUP'):
686 sys.exit(0)
690 sys.exit(0)
687 while True:
691 while True:
688 timeout = None
692 timeout = None
689 timeobj = None
693 timeobj = None
690 for obj in self.table.itervalues():
694 for obj in self.table.itervalues():
691 if obj.timeout is not None and (timeout is None or obj.timeout < timeout):
695 if obj.timeout is not None and (timeout is None or obj.timeout < timeout):
692 timeout, timeobj = obj.timeout, obj
696 timeout, timeobj = obj.timeout, obj
693 try:
697 try:
694 if self.ui.debugflag:
698 if self.ui.debugflag:
695 if timeout is None:
699 if timeout is None:
696 self.ui.note(_('polling: no timeout\n'))
700 self.ui.note(_('polling: no timeout\n'))
697 else:
701 else:
698 self.ui.note(_('polling: %sms timeout\n') % timeout)
702 self.ui.note(_('polling: %sms timeout\n') % timeout)
699 events = self.poll.poll(timeout)
703 events = self.poll.poll(timeout)
700 except select.error, err:
704 except select.error, err:
701 if err[0] == errno.EINTR:
705 if err[0] == errno.EINTR:
702 continue
706 continue
703 raise
707 raise
704 if events:
708 if events:
705 for fd, event in events:
709 for fd, event in events:
706 self.table[fd].handle_event(fd, event)
710 self.table[fd].handle_event(fd, event)
707 elif timeobj:
711 elif timeobj:
708 timeobj.handle_timeout()
712 timeobj.handle_timeout()
709
713
710 def start(ui, repo):
714 def start(ui, repo):
711 def closefds(ignore):
715 def closefds(ignore):
712 # (from python bug #1177468)
716 # (from python bug #1177468)
713 # close all inherited file descriptors
717 # close all inherited file descriptors
714 # Python 2.4.1 and later use /dev/urandom to seed the random module's RNG
718 # Python 2.4.1 and later use /dev/urandom to seed the random module's RNG
715 # a file descriptor is kept internally as os._urandomfd (created on demand
719 # a file descriptor is kept internally as os._urandomfd (created on demand
716 # the first time os.urandom() is called), and should not be closed
720 # the first time os.urandom() is called), and should not be closed
717 try:
721 try:
718 os.urandom(4)
722 os.urandom(4)
719 urandom_fd = getattr(os, '_urandomfd', None)
723 urandom_fd = getattr(os, '_urandomfd', None)
720 except AttributeError:
724 except AttributeError:
721 urandom_fd = None
725 urandom_fd = None
722 ignore.append(urandom_fd)
726 ignore.append(urandom_fd)
723 for fd in range(3, 256):
727 for fd in range(3, 256):
724 if fd in ignore:
728 if fd in ignore:
725 continue
729 continue
726 try:
730 try:
727 os.close(fd)
731 os.close(fd)
728 except OSError:
732 except OSError:
729 pass
733 pass
730
734
731 m = Master(ui, repo)
735 m = Master(ui, repo)
732 sys.stdout.flush()
736 sys.stdout.flush()
733 sys.stderr.flush()
737 sys.stderr.flush()
734
738
735 pid = os.fork()
739 pid = os.fork()
736 if pid:
740 if pid:
737 return pid
741 return pid
738
742
739 closefds([m.server.fileno(), m.watcher.fileno()])
743 closefds([m.server.fileno(), m.watcher.fileno()])
740 os.setsid()
744 os.setsid()
741
745
742 fd = os.open('/dev/null', os.O_RDONLY)
746 fd = os.open('/dev/null', os.O_RDONLY)
743 os.dup2(fd, 0)
747 os.dup2(fd, 0)
744 if fd > 0:
748 if fd > 0:
745 os.close(fd)
749 os.close(fd)
746
750
747 fd = os.open(ui.config('inotify', 'log', '/dev/null'),
751 fd = os.open(ui.config('inotify', 'log', '/dev/null'),
748 os.O_RDWR | os.O_CREAT | os.O_TRUNC)
752 os.O_RDWR | os.O_CREAT | os.O_TRUNC)
749 os.dup2(fd, 1)
753 os.dup2(fd, 1)
750 os.dup2(fd, 2)
754 os.dup2(fd, 2)
751 if fd > 2:
755 if fd > 2:
752 os.close(fd)
756 os.close(fd)
753
757
754 try:
758 try:
755 m.run()
759 m.run()
756 finally:
760 finally:
757 m.shutdown()
761 m.shutdown()
758 os._exit(0)
762 os._exit(0)
General Comments 0
You need to be logged in to leave comments. Login now