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