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