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