##// END OF EJS Templates
blackbox: guard against recursion from dirty check
timeless -
r28407:63da8bd0 default
parent child Browse files
Show More
@@ -1,239 +1,250 b''
1 1 # blackbox.py - log repository events to a file for post-mortem debugging
2 2 #
3 3 # Copyright 2010 Nicolas Dumazet
4 4 # Copyright 2013 Facebook, Inc.
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """log repository events to a blackbox for debugging
10 10
11 11 Logs event information to .hg/blackbox.log to help debug and diagnose problems.
12 12 The events that get logged can be configured via the blackbox.track config key.
13 13
14 14 Examples::
15 15
16 16 [blackbox]
17 17 track = *
18 18 # dirty is *EXPENSIVE* (slow);
19 19 # each log entry indicates `+` if the repository is dirty, like :hg:`id`.
20 20 dirty = True
21 21 # record the source of log messages
22 22 logsource = True
23 23
24 24 [blackbox]
25 25 track = command, commandfinish, commandexception, exthook, pythonhook
26 26
27 27 [blackbox]
28 28 track = incoming
29 29
30 30 [blackbox]
31 31 # limit the size of a log file
32 32 maxsize = 1.5 MB
33 33 # rotate up to N log files when the current one gets too big
34 34 maxfiles = 3
35 35
36 36 """
37 37
38 38 from __future__ import absolute_import
39 39
40 40 import errno
41 41 import re
42 42
43 43 from mercurial.i18n import _
44 44 from mercurial.node import hex
45 45
46 46 from mercurial import (
47 47 cmdutil,
48 48 ui as uimod,
49 49 util,
50 50 )
51 51
52 52 cmdtable = {}
53 53 command = cmdutil.command(cmdtable)
54 54 # Note for extension authors: ONLY specify testedwith = 'internal' for
55 55 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
56 56 # be specifying the version(s) of Mercurial they are tested with, or
57 57 # leave the attribute unspecified.
58 58 testedwith = 'internal'
59 59 lastui = None
60 60
61 61 filehandles = {}
62 62
63 63 def _openlog(vfs):
64 64 path = vfs.join('blackbox.log')
65 65 if path in filehandles:
66 66 return filehandles[path]
67 67 filehandles[path] = fp = vfs('blackbox.log', 'a')
68 68 return fp
69 69
70 70 def _closelog(vfs):
71 71 path = vfs.join('blackbox.log')
72 72 fp = filehandles[path]
73 73 del filehandles[path]
74 74 fp.close()
75 75
76 76 def wrapui(ui):
77 77 class blackboxui(ui.__class__):
78 78 def __init__(self, src=None):
79 79 super(blackboxui, self).__init__(src)
80 80 if src is None:
81 81 self._partialinit()
82 82 else:
83 83 self._bbfp = src._bbfp
84 self._bbinlog = False
84 85 self._bbrepo = src._bbrepo
85 86 self._bbvfs = src._bbvfs
86 87
87 88 def _partialinit(self):
88 89 if util.safehasattr(self, '_bbvfs'):
89 90 return
90 91 self._bbfp = None
92 self._bbinlog = False
91 93 self._bbrepo = None
92 94 self._bbvfs = None
93 95
94 96 def copy(self):
95 97 self._partialinit()
96 98 return self.__class__(self)
97 99
98 100 @util.propertycache
99 101 def track(self):
100 102 return self.configlist('blackbox', 'track', ['*'])
101 103
102 104 def _openlogfile(self):
103 105 def rotate(oldpath, newpath):
104 106 try:
105 107 self._bbvfs.unlink(newpath)
106 108 except OSError as err:
107 109 if err.errno != errno.ENOENT:
108 110 self.debug("warning: cannot remove '%s': %s\n" %
109 111 (newpath, err.strerror))
110 112 try:
111 113 if newpath:
112 114 self._bbvfs.rename(oldpath, newpath)
113 115 except OSError as err:
114 116 if err.errno != errno.ENOENT:
115 117 self.debug("warning: cannot rename '%s' to '%s': %s\n" %
116 118 (newpath, oldpath, err.strerror))
117 119
118 120 fp = _openlog(self._bbvfs)
119 121 maxsize = self.configbytes('blackbox', 'maxsize', 1048576)
120 122 if maxsize > 0:
121 123 st = self._bbvfs.fstat(fp)
122 124 if st.st_size >= maxsize:
123 125 path = fp.name
124 126 _closelog(self._bbvfs)
125 127 maxfiles = self.configint('blackbox', 'maxfiles', 7)
126 128 for i in xrange(maxfiles - 1, 1, -1):
127 129 rotate(oldpath='%s.%d' % (path, i - 1),
128 130 newpath='%s.%d' % (path, i))
129 131 rotate(oldpath=path,
130 132 newpath=maxfiles > 0 and path + '.1')
131 133 fp = _openlog(self._bbvfs)
132 134 return fp
133 135
134 136 def _bbwrite(self, fmt, *args):
135 137 self._bbfp.write(fmt % args)
136 138 self._bbfp.flush()
137 139
138 140 def log(self, event, *msg, **opts):
139 141 global lastui
140 142 super(blackboxui, self).log(event, *msg, **opts)
141 143 self._partialinit()
142 144
143 145 if not '*' in self.track and not event in self.track:
144 146 return
145 147
146 148 if self._bbfp:
147 149 ui = self
148 150 elif self._bbvfs:
149 151 try:
150 152 self._bbfp = self._openlogfile()
151 153 except (IOError, OSError) as err:
152 154 self.debug('warning: cannot write to blackbox.log: %s\n' %
153 155 err.strerror)
154 156 del self._bbvfs
155 157 self._bbfp = None
156 158 ui = self
157 159 else:
158 160 # certain ui instances exist outside the context of
159 161 # a repo, so just default to the last blackbox that
160 162 # was seen.
161 163 ui = lastui
162 164
163 if ui and ui._bbfp:
165 if not ui or not ui._bbfp:
166 return
167 if not lastui or ui._bbrepo:
168 lastui = ui
169 if ui._bbinlog:
170 # recursion guard
171 return
172 try:
173 ui._bbinlog = True
164 174 date = util.datestr(None, '%Y/%m/%d %H:%M:%S')
165 175 user = util.getuser()
166 176 pid = str(util.getpid())
167 177 formattedmsg = msg[0] % msg[1:]
168 178 rev = '(unknown)'
169 179 changed = ''
170 180 if ui._bbrepo:
171 181 ctx = ui._bbrepo[None]
172 182 parents = ctx.parents()
173 183 rev = ('+'.join([hex(p.node()) for p in parents]))
174 184 if (ui.configbool('blackbox', 'dirty', False) and (
175 185 any(ui._bbrepo.status()) or
176 186 any(ctx.sub(s).dirty() for s in ctx.substate)
177 187 )):
178 188 changed = '+'
179 189 if ui.configbool('blackbox', 'logsource', False):
180 190 src = ' [%s]' % event
181 191 else:
182 192 src = ''
183 193 try:
184 194 ui._bbwrite('%s %s @%s%s (%s)%s> %s',
185 195 date, user, rev, changed, pid, src, formattedmsg)
186 196 except IOError as err:
187 197 self.debug('warning: cannot write to blackbox.log: %s\n' %
188 198 err.strerror)
189 if not lastui or ui._bbrepo:
190 lastui = ui
199 finally:
200 ui._bbinlog = False
191 201
192 202 def setrepo(self, repo):
193 203 self._bbfp = None
204 self._bbinlog = False
194 205 self._bbrepo = repo
195 206 self._bbvfs = repo.vfs
196 207
197 208 ui.__class__ = blackboxui
198 209 uimod.ui = blackboxui
199 210
200 211 def uisetup(ui):
201 212 wrapui(ui)
202 213
203 214 def reposetup(ui, repo):
204 215 # During 'hg pull' a httppeer repo is created to represent the remote repo.
205 216 # It doesn't have a .hg directory to put a blackbox in, so we don't do
206 217 # the blackbox setup for it.
207 218 if not repo.local():
208 219 return
209 220
210 221 if util.safehasattr(ui, 'setrepo'):
211 222 ui.setrepo(repo)
212 223
213 224 @command('^blackbox',
214 225 [('l', 'limit', 10, _('the number of events to show')),
215 226 ],
216 227 _('hg blackbox [OPTION]...'))
217 228 def blackbox(ui, repo, *revs, **opts):
218 229 '''view the recent repository events
219 230 '''
220 231
221 232 if not repo.vfs.exists('blackbox.log'):
222 233 return
223 234
224 235 limit = opts.get('limit')
225 236 fp = repo.vfs('blackbox.log', 'r')
226 237 lines = fp.read().split('\n')
227 238
228 239 count = 0
229 240 output = []
230 241 for line in reversed(lines):
231 242 if count >= limit:
232 243 break
233 244
234 245 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
235 246 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
236 247 count += 1
237 248 output.append(line)
238 249
239 250 ui.status('\n'.join(reversed(output)))
@@ -1,195 +1,209 b''
1 1 setup
2 2 $ cat >> $HGRCPATH <<EOF
3 3 > [extensions]
4 4 > blackbox=
5 5 > mock=$TESTDIR/mockblackbox.py
6 6 > mq=
7 7 > EOF
8 8 $ hg init blackboxtest
9 9 $ cd blackboxtest
10 10
11 11 command, exit codes, and duration
12 12
13 13 $ echo a > a
14 14 $ hg add a
15 15 $ hg blackbox --config blackbox.dirty=True
16 16 1970/01/01 00:00:00 bob @0000000000000000000000000000000000000000 (5000)> add a
17 17 1970/01/01 00:00:00 bob @0000000000000000000000000000000000000000 (5000)> add a exited 0 after * seconds (glob)
18 18 1970/01/01 00:00:00 bob @0000000000000000000000000000000000000000+ (5000)> blackbox
19 19
20 20 incoming change tracking
21 21
22 22 create two heads to verify that we only see one change in the log later
23 23 $ hg commit -ma
24 24 $ hg up null
25 25 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
26 26 $ echo b > b
27 27 $ hg commit -Amb
28 28 adding b
29 29 created new head
30 30
31 31 clone, commit, pull
32 32 $ hg clone . ../blackboxtest2
33 33 updating to branch default
34 34 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
35 35 $ echo c > c
36 36 $ hg commit -Amc
37 37 adding c
38 38 $ cd ../blackboxtest2
39 39 $ hg pull
40 40 pulling from $TESTTMP/blackboxtest (glob)
41 41 searching for changes
42 42 adding changesets
43 43 adding manifests
44 44 adding file changes
45 45 added 1 changesets with 1 changes to 1 files
46 46 (run 'hg update' to get a working copy)
47 47 $ hg blackbox -l 6
48 48 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> pull
49 49 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> updated served branch cache in * seconds (glob)
50 50 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> wrote served branch cache with 1 labels and 2 nodes
51 51 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> 1 incoming changes - new heads: d02f48003e62
52 52 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> pull exited 0 after * seconds (glob)
53 53 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> blackbox -l 6
54 54
55 55 we must not cause a failure if we cannot write to the log
56 56
57 57 $ hg rollback
58 58 repository tip rolled back to revision 1 (undo pull)
59 59
60 60 $ mv .hg/blackbox.log .hg/blackbox.log-
61 61 $ mkdir .hg/blackbox.log
62 62 $ hg --debug incoming
63 63 warning: cannot write to blackbox.log: * (glob)
64 64 comparing with $TESTTMP/blackboxtest (glob)
65 65 query 1; heads
66 66 searching for changes
67 67 all local heads known remotely
68 68 changeset: 2:d02f48003e62c24e2659d97d30f2a83abe5d5d51
69 69 tag: tip
70 70 phase: draft
71 71 parent: 1:6563da9dcf87b1949716e38ff3e3dfaa3198eb06
72 72 parent: -1:0000000000000000000000000000000000000000
73 73 manifest: 2:ab9d46b053ebf45b7996f2922b9893ff4b63d892
74 74 user: test
75 75 date: Thu Jan 01 00:00:00 1970 +0000
76 76 files+: c
77 77 extra: branch=default
78 78 description:
79 79 c
80 80
81 81
82 82 $ hg pull
83 83 pulling from $TESTTMP/blackboxtest (glob)
84 84 searching for changes
85 85 adding changesets
86 86 adding manifests
87 87 adding file changes
88 88 added 1 changesets with 1 changes to 1 files
89 89 (run 'hg update' to get a working copy)
90 90
91 91 a failure reading from the log is fatal
92 92
93 93 $ hg blackbox -l 3
94 94 abort: *$TESTTMP/blackboxtest2/.hg/blackbox.log* (glob)
95 95 [255]
96 96
97 97 $ rmdir .hg/blackbox.log
98 98 $ mv .hg/blackbox.log- .hg/blackbox.log
99 99
100 100 backup bundles get logged
101 101
102 102 $ touch d
103 103 $ hg commit -Amd
104 104 adding d
105 105 created new head
106 106 $ hg strip tip
107 107 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
108 108 saved backup bundle to $TESTTMP/blackboxtest2/.hg/strip-backup/*-backup.hg (glob)
109 109 $ hg blackbox -l 6
110 110 1970/01/01 00:00:00 bob @73f6ee326b27d820b0472f1a825e3a50f3dc489b (5000)> strip tip
111 111 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> saved backup bundle to $TESTTMP/blackboxtest2/.hg/strip-backup/73f6ee326b27-7612e004-backup.hg
112 112 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> updated base branch cache in * seconds (glob)
113 113 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> wrote base branch cache with 1 labels and 2 nodes
114 114 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> strip tip exited 0 after * seconds (glob)
115 115 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> blackbox -l 6
116 116
117 117 extension and python hooks - use the eol extension for a pythonhook
118 118
119 119 $ echo '[extensions]' >> .hg/hgrc
120 120 $ echo 'eol=' >> .hg/hgrc
121 121 $ echo '[hooks]' >> .hg/hgrc
122 122 $ echo 'update = echo hooked' >> .hg/hgrc
123 123 $ hg update
124 124 hooked
125 125 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
126 126 1 other heads for branch "default"
127 127 $ hg blackbox -l 6
128 128 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> update
129 129 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> writing .hg/cache/tags2-visible with 0 tags
130 130 1970/01/01 00:00:00 bob @6563da9dcf87b1949716e38ff3e3dfaa3198eb06 (5000)> pythonhook-preupdate: hgext.eol.preupdate finished in * seconds (glob)
131 131 1970/01/01 00:00:00 bob @d02f48003e62c24e2659d97d30f2a83abe5d5d51 (5000)> exthook-update: echo hooked finished in * seconds (glob)
132 132 1970/01/01 00:00:00 bob @d02f48003e62c24e2659d97d30f2a83abe5d5d51 (5000)> update exited 0 after * seconds (glob)
133 133 1970/01/01 00:00:00 bob @d02f48003e62c24e2659d97d30f2a83abe5d5d51 (5000)> blackbox -l 6
134 134
135 135 log rotation
136 136
137 137 $ echo '[blackbox]' >> .hg/hgrc
138 138 $ echo 'maxsize = 20 b' >> .hg/hgrc
139 139 $ echo 'maxfiles = 3' >> .hg/hgrc
140 140 $ hg status
141 141 $ hg status
142 142 $ hg status
143 143 $ hg tip -q
144 144 2:d02f48003e62
145 145 $ ls .hg/blackbox.log*
146 146 .hg/blackbox.log
147 147 .hg/blackbox.log.1
148 148 .hg/blackbox.log.2
149 149 $ cd ..
150 150
151 151 $ hg init blackboxtest3
152 152 $ cd blackboxtest3
153 153 $ hg blackbox
154 154 1970/01/01 00:00:00 bob @0000000000000000000000000000000000000000 (5000)> blackbox
155 155 $ mv .hg/blackbox.log .hg/blackbox.log-
156 156 $ mkdir .hg/blackbox.log
157 157 $ sed -e 's/\(.*test1.*\)/#\1/; s#\(.*commit2.*\)#os.rmdir(".hg/blackbox.log")\
158 158 > os.rename(".hg/blackbox.log-", ".hg/blackbox.log")\
159 159 > \1#' $TESTDIR/test-dispatch.py > ../test-dispatch.py
160 160 $ python $TESTDIR/blackbox-readonly-dispatch.py
161 161 running: add foo
162 162 result: 0
163 163 running: commit -m commit1 -d 2000-01-01 foo
164 164 result: None
165 165 running: commit -m commit2 -d 2000-01-02 foo
166 166 result: None
167 167 running: log -r 0
168 168 changeset: 0:0e4634943879
169 169 user: test
170 170 date: Sat Jan 01 00:00:00 2000 +0000
171 171 summary: commit1
172 172
173 173 result: None
174 174 running: log -r tip
175 175 changeset: 1:45589e459b2e
176 176 tag: tip
177 177 user: test
178 178 date: Sun Jan 02 00:00:00 2000 +0000
179 179 summary: commit2
180 180
181 181 result: None
182 182 $ hg blackbox
183 183 1970/01/01 00:00:00 bob @0e46349438790c460c5c9f7546bfcd39b267bbd2 (5000)> commit -m commit2 -d 2000-01-02 foo
184 184 1970/01/01 00:00:00 bob @0e46349438790c460c5c9f7546bfcd39b267bbd2 (5000)> updated served branch cache in * seconds (glob)
185 185 1970/01/01 00:00:00 bob @0e46349438790c460c5c9f7546bfcd39b267bbd2 (5000)> wrote served branch cache with 1 labels and 1 nodes
186 186 1970/01/01 00:00:00 bob @45589e459b2edfbf3dbde7e01f611d2c1e7453d7 (5000)> commit -m commit2 -d 2000-01-02 foo exited 0 after * seconds (glob)
187 187 1970/01/01 00:00:00 bob @45589e459b2edfbf3dbde7e01f611d2c1e7453d7 (5000)> log -r 0
188 188 1970/01/01 00:00:00 bob @45589e459b2edfbf3dbde7e01f611d2c1e7453d7 (5000)> writing .hg/cache/tags2-visible with 0 tags
189 189 1970/01/01 00:00:00 bob @45589e459b2edfbf3dbde7e01f611d2c1e7453d7 (5000)> log -r 0 exited 0 after * seconds (glob)
190 190 1970/01/01 00:00:00 bob @45589e459b2edfbf3dbde7e01f611d2c1e7453d7 (5000)> log -r tip
191 191 1970/01/01 00:00:00 bob @45589e459b2edfbf3dbde7e01f611d2c1e7453d7 (5000)> log -r tip exited 0 after * seconds (glob)
192 192 1970/01/01 00:00:00 bob @45589e459b2edfbf3dbde7e01f611d2c1e7453d7 (5000)> blackbox
193 193
194 Test log recursion from dirty status check
195
196 $ cat > ../r.py <<EOF
197 > from mercurial import context, error, extensions
198 > x=[False]
199 > def status(orig, *args, **opts):
200 > args[0].repo().ui.log("broken", "recursion?")
201 > return orig(*args, **opts)
202 > def reposetup(ui, repo):
203 > extensions.wrapfunction(context.basectx, 'status', status)
204 > EOF
205 $ hg id --config extensions.x=../r.py --config blackbox.dirty=True
206 45589e459b2e tip
207
194 208 cleanup
195 209 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now