mirror of
https://github.com/facebook/sapling.git
synced 2024-10-10 00:45:18 +03:00
4e1c384d0a
I've caught multiple extensions in the wild lying about being 'internal', so it's time to move the goalposts on people. Goalpost moving will continue until third party extensions stop trying to defeat the system.
251 lines
7.8 KiB
Python
251 lines
7.8 KiB
Python
# blackbox.py - log repository events to a file for post-mortem debugging
|
|
#
|
|
# Copyright 2010 Nicolas Dumazet
|
|
# Copyright 2013 Facebook, Inc.
|
|
#
|
|
# This software may be used and distributed according to the terms of the
|
|
# GNU General Public License version 2 or any later version.
|
|
|
|
"""log repository events to a blackbox for debugging
|
|
|
|
Logs event information to .hg/blackbox.log to help debug and diagnose problems.
|
|
The events that get logged can be configured via the blackbox.track config key.
|
|
|
|
Examples::
|
|
|
|
[blackbox]
|
|
track = *
|
|
# dirty is *EXPENSIVE* (slow);
|
|
# each log entry indicates `+` if the repository is dirty, like :hg:`id`.
|
|
dirty = True
|
|
# record the source of log messages
|
|
logsource = True
|
|
|
|
[blackbox]
|
|
track = command, commandfinish, commandexception, exthook, pythonhook
|
|
|
|
[blackbox]
|
|
track = incoming
|
|
|
|
[blackbox]
|
|
# limit the size of a log file
|
|
maxsize = 1.5 MB
|
|
# rotate up to N log files when the current one gets too big
|
|
maxfiles = 3
|
|
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import errno
|
|
import re
|
|
|
|
from mercurial.i18n import _
|
|
from mercurial.node import hex
|
|
|
|
from mercurial import (
|
|
cmdutil,
|
|
ui as uimod,
|
|
util,
|
|
)
|
|
|
|
cmdtable = {}
|
|
command = cmdutil.command(cmdtable)
|
|
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
|
|
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
|
|
# be specifying the version(s) of Mercurial they are tested with, or
|
|
# leave the attribute unspecified.
|
|
testedwith = 'ships-with-hg-core'
|
|
lastui = None
|
|
|
|
filehandles = {}
|
|
|
|
def _openlog(vfs):
|
|
path = vfs.join('blackbox.log')
|
|
if path in filehandles:
|
|
return filehandles[path]
|
|
filehandles[path] = fp = vfs('blackbox.log', 'a')
|
|
return fp
|
|
|
|
def _closelog(vfs):
|
|
path = vfs.join('blackbox.log')
|
|
fp = filehandles[path]
|
|
del filehandles[path]
|
|
fp.close()
|
|
|
|
def wrapui(ui):
|
|
class blackboxui(ui.__class__):
|
|
def __init__(self, src=None):
|
|
super(blackboxui, self).__init__(src)
|
|
if src is None:
|
|
self._partialinit()
|
|
else:
|
|
self._bbfp = getattr(src, '_bbfp', None)
|
|
self._bbinlog = False
|
|
self._bbrepo = getattr(src, '_bbrepo', None)
|
|
self._bbvfs = getattr(src, '_bbvfs', None)
|
|
|
|
def _partialinit(self):
|
|
if util.safehasattr(self, '_bbvfs'):
|
|
return
|
|
self._bbfp = None
|
|
self._bbinlog = False
|
|
self._bbrepo = None
|
|
self._bbvfs = None
|
|
|
|
def copy(self):
|
|
self._partialinit()
|
|
return self.__class__(self)
|
|
|
|
@util.propertycache
|
|
def track(self):
|
|
return self.configlist('blackbox', 'track', ['*'])
|
|
|
|
def _openlogfile(self):
|
|
def rotate(oldpath, newpath):
|
|
try:
|
|
self._bbvfs.unlink(newpath)
|
|
except OSError as err:
|
|
if err.errno != errno.ENOENT:
|
|
self.debug("warning: cannot remove '%s': %s\n" %
|
|
(newpath, err.strerror))
|
|
try:
|
|
if newpath:
|
|
self._bbvfs.rename(oldpath, newpath)
|
|
except OSError as err:
|
|
if err.errno != errno.ENOENT:
|
|
self.debug("warning: cannot rename '%s' to '%s': %s\n" %
|
|
(newpath, oldpath, err.strerror))
|
|
|
|
fp = _openlog(self._bbvfs)
|
|
maxsize = self.configbytes('blackbox', 'maxsize', 1048576)
|
|
if maxsize > 0:
|
|
st = self._bbvfs.fstat(fp)
|
|
if st.st_size >= maxsize:
|
|
path = fp.name
|
|
_closelog(self._bbvfs)
|
|
maxfiles = self.configint('blackbox', 'maxfiles', 7)
|
|
for i in xrange(maxfiles - 1, 1, -1):
|
|
rotate(oldpath='%s.%d' % (path, i - 1),
|
|
newpath='%s.%d' % (path, i))
|
|
rotate(oldpath=path,
|
|
newpath=maxfiles > 0 and path + '.1')
|
|
fp = _openlog(self._bbvfs)
|
|
return fp
|
|
|
|
def _bbwrite(self, fmt, *args):
|
|
self._bbfp.write(fmt % args)
|
|
self._bbfp.flush()
|
|
|
|
def log(self, event, *msg, **opts):
|
|
global lastui
|
|
super(blackboxui, self).log(event, *msg, **opts)
|
|
self._partialinit()
|
|
|
|
if not '*' in self.track and not event in self.track:
|
|
return
|
|
|
|
if self._bbfp:
|
|
ui = self
|
|
elif self._bbvfs:
|
|
try:
|
|
self._bbfp = self._openlogfile()
|
|
except (IOError, OSError) as err:
|
|
self.debug('warning: cannot write to blackbox.log: %s\n' %
|
|
err.strerror)
|
|
del self._bbvfs
|
|
self._bbfp = None
|
|
ui = self
|
|
else:
|
|
# certain ui instances exist outside the context of
|
|
# a repo, so just default to the last blackbox that
|
|
# was seen.
|
|
ui = lastui
|
|
|
|
if not ui or not ui._bbfp:
|
|
return
|
|
if not lastui or ui._bbrepo:
|
|
lastui = ui
|
|
if ui._bbinlog:
|
|
# recursion guard
|
|
return
|
|
try:
|
|
ui._bbinlog = True
|
|
date = util.datestr(None, '%Y/%m/%d %H:%M:%S')
|
|
user = util.getuser()
|
|
pid = str(util.getpid())
|
|
formattedmsg = msg[0] % msg[1:]
|
|
rev = '(unknown)'
|
|
changed = ''
|
|
if ui._bbrepo:
|
|
ctx = ui._bbrepo[None]
|
|
parents = ctx.parents()
|
|
rev = ('+'.join([hex(p.node()) for p in parents]))
|
|
if (ui.configbool('blackbox', 'dirty', False) and (
|
|
any(ui._bbrepo.status()) or
|
|
any(ctx.sub(s).dirty() for s in ctx.substate)
|
|
)):
|
|
changed = '+'
|
|
if ui.configbool('blackbox', 'logsource', False):
|
|
src = ' [%s]' % event
|
|
else:
|
|
src = ''
|
|
try:
|
|
ui._bbwrite('%s %s @%s%s (%s)%s> %s',
|
|
date, user, rev, changed, pid, src, formattedmsg)
|
|
except IOError as err:
|
|
self.debug('warning: cannot write to blackbox.log: %s\n' %
|
|
err.strerror)
|
|
finally:
|
|
ui._bbinlog = False
|
|
|
|
def setrepo(self, repo):
|
|
self._bbfp = None
|
|
self._bbinlog = False
|
|
self._bbrepo = repo
|
|
self._bbvfs = repo.vfs
|
|
|
|
ui.__class__ = blackboxui
|
|
uimod.ui = blackboxui
|
|
|
|
def uisetup(ui):
|
|
wrapui(ui)
|
|
|
|
def reposetup(ui, repo):
|
|
# During 'hg pull' a httppeer repo is created to represent the remote repo.
|
|
# It doesn't have a .hg directory to put a blackbox in, so we don't do
|
|
# the blackbox setup for it.
|
|
if not repo.local():
|
|
return
|
|
|
|
if util.safehasattr(ui, 'setrepo'):
|
|
ui.setrepo(repo)
|
|
|
|
@command('^blackbox',
|
|
[('l', 'limit', 10, _('the number of events to show')),
|
|
],
|
|
_('hg blackbox [OPTION]...'))
|
|
def blackbox(ui, repo, *revs, **opts):
|
|
'''view the recent repository events
|
|
'''
|
|
|
|
if not repo.vfs.exists('blackbox.log'):
|
|
return
|
|
|
|
limit = opts.get('limit')
|
|
fp = repo.vfs('blackbox.log', 'r')
|
|
lines = fp.read().split('\n')
|
|
|
|
count = 0
|
|
output = []
|
|
for line in reversed(lines):
|
|
if count >= limit:
|
|
break
|
|
|
|
# count the commands by matching lines like: 2013/01/23 19:13:36 root>
|
|
if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
|
|
count += 1
|
|
output.append(line)
|
|
|
|
ui.status('\n'.join(reversed(output)))
|