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