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