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