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