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