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