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