Set tracing to always on and save to .repo/TRACE_FILE.

- add `--trace_to_stderr` option so stderr will include trace outputs and any other errors that get sent to stderr
- while TRACE_FILE will only include trace outputs

piggy-backing on: https://gerrit-review.googlesource.com/c/git-repo/+/349154

Change-Id: I3895a84de4b2784f17fac4325521cd5e72e645e2
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/350114
Reviewed-by: LaMont Jones <lamontjones@google.com>
Tested-by: Joanna Wang <jojwang@google.com>
This commit is contained in:
Joanna Wang 2022-11-03 16:51:19 -04:00
parent 0d130d2da0
commit a6c52f566a
13 changed files with 240 additions and 106 deletions

View File

@ -230,12 +230,11 @@ class GitCommand(object):
stderr = (subprocess.STDOUT if merge_output else stderr = (subprocess.STDOUT if merge_output else
(subprocess.PIPE if capture_stderr else None)) (subprocess.PIPE if capture_stderr else None))
dbg = ''
if IsTrace(): if IsTrace():
global LAST_CWD global LAST_CWD
global LAST_GITDIR global LAST_GITDIR
dbg = ''
if cwd and LAST_CWD != cwd: if cwd and LAST_CWD != cwd:
if LAST_GITDIR or LAST_CWD: if LAST_GITDIR or LAST_CWD:
dbg += '\n' dbg += '\n'
@ -263,31 +262,31 @@ class GitCommand(object):
dbg += ' 2>|' dbg += ' 2>|'
elif stderr == subprocess.STDOUT: elif stderr == subprocess.STDOUT:
dbg += ' 2>&1' dbg += ' 2>&1'
Trace('%s', dbg)
try: with Trace('git command %s %s with debug: %s', LAST_GITDIR, command, dbg):
p = subprocess.Popen(command, try:
cwd=cwd, p = subprocess.Popen(command,
env=env, cwd=cwd,
encoding='utf-8', env=env,
errors='backslashreplace', encoding='utf-8',
stdin=stdin, errors='backslashreplace',
stdout=stdout, stdin=stdin,
stderr=stderr) stdout=stdout,
except Exception as e: stderr=stderr)
raise GitError('%s: %s' % (command[1], e)) except Exception as e:
raise GitError('%s: %s' % (command[1], e))
if ssh_proxy:
ssh_proxy.add_client(p)
self.process = p
try:
self.stdout, self.stderr = p.communicate(input=input)
finally:
if ssh_proxy: if ssh_proxy:
ssh_proxy.remove_client(p) ssh_proxy.add_client(p)
self.rc = p.wait()
self.process = p
try:
self.stdout, self.stderr = p.communicate(input=input)
finally:
if ssh_proxy:
ssh_proxy.remove_client(p)
self.rc = p.wait()
@staticmethod @staticmethod
def _GetBasicEnv(): def _GetBasicEnv():

View File

@ -349,9 +349,9 @@ class GitConfig(object):
except OSError: except OSError:
return None return None
try: try:
Trace(': parsing %s', self.file) with Trace(': parsing %s', self.file):
with open(self._json) as fd: with open(self._json) as fd:
return json.load(fd) return json.load(fd)
except (IOError, ValueError): except (IOError, ValueError):
platform_utils.remove(self._json, missing_ok=True) platform_utils.remove(self._json, missing_ok=True)
return None return None

View File

@ -67,38 +67,37 @@ class GitRefs(object):
self._LoadAll() self._LoadAll()
def _NeedUpdate(self): def _NeedUpdate(self):
Trace(': scan refs %s', self._gitdir) with Trace(': scan refs %s', self._gitdir):
for name, mtime in self._mtime.items():
for name, mtime in self._mtime.items(): try:
try: if mtime != os.path.getmtime(os.path.join(self._gitdir, name)):
if mtime != os.path.getmtime(os.path.join(self._gitdir, name)): return True
except OSError:
return True return True
except OSError: return False
return True
return False
def _LoadAll(self): def _LoadAll(self):
Trace(': load refs %s', self._gitdir) with Trace(': load refs %s', self._gitdir):
self._phyref = {} self._phyref = {}
self._symref = {} self._symref = {}
self._mtime = {} self._mtime = {}
self._ReadPackedRefs() self._ReadPackedRefs()
self._ReadLoose('refs/') self._ReadLoose('refs/')
self._ReadLoose1(os.path.join(self._gitdir, HEAD), HEAD) self._ReadLoose1(os.path.join(self._gitdir, HEAD), HEAD)
scan = self._symref scan = self._symref
attempts = 0 attempts = 0
while scan and attempts < 5: while scan and attempts < 5:
scan_next = {} scan_next = {}
for name, dest in scan.items(): for name, dest in scan.items():
if dest in self._phyref: if dest in self._phyref:
self._phyref[name] = self._phyref[dest] self._phyref[name] = self._phyref[dest]
else: else:
scan_next[name] = dest scan_next[name] = dest
scan = scan_next scan = scan_next
attempts += 1 attempts += 1
def _ReadPackedRefs(self): def _ReadPackedRefs(self):
path = os.path.join(self._gitdir, 'packed-refs') path = os.path.join(self._gitdir, 'packed-refs')

33
main.py
View File

@ -37,7 +37,7 @@ except ImportError:
from color import SetDefaultColoring from color import SetDefaultColoring
import event_log import event_log
from repo_trace import SetTrace from repo_trace import SetTrace, Trace, SetTraceToStderr
from git_command import user_agent from git_command import user_agent
from git_config import RepoConfig from git_config import RepoConfig
from git_trace2_event_log import EventLog from git_trace2_event_log import EventLog
@ -109,6 +109,9 @@ global_options.add_option('--color',
global_options.add_option('--trace', global_options.add_option('--trace',
dest='trace', action='store_true', dest='trace', action='store_true',
help='trace git command execution (REPO_TRACE=1)') help='trace git command execution (REPO_TRACE=1)')
global_options.add_option('--trace_to_stderr',
dest='trace_to_stderr', action='store_true',
help='trace outputs go to stderr in addition to .repo/TRACE_FILE')
global_options.add_option('--trace-python', global_options.add_option('--trace-python',
dest='trace_python', action='store_true', dest='trace_python', action='store_true',
help='trace python command execution') help='trace python command execution')
@ -198,9 +201,6 @@ class _Repo(object):
"""Execute the requested subcommand.""" """Execute the requested subcommand."""
result = 0 result = 0
if gopts.trace:
SetTrace()
# Handle options that terminate quickly first. # Handle options that terminate quickly first.
if gopts.help or gopts.help_all: if gopts.help or gopts.help_all:
self._PrintHelp(short=False, all_commands=gopts.help_all) self._PrintHelp(short=False, all_commands=gopts.help_all)
@ -652,17 +652,26 @@ def _Main(argv):
Version.wrapper_path = opt.wrapper_path Version.wrapper_path = opt.wrapper_path
repo = _Repo(opt.repodir) repo = _Repo(opt.repodir)
try: try:
init_http() init_http()
name, gopts, argv = repo._ParseArgs(argv) name, gopts, argv = repo._ParseArgs(argv)
run = lambda: repo._Run(name, gopts, argv) or 0
if gopts.trace_python: if gopts.trace:
import trace SetTrace()
tracer = trace.Trace(count=False, trace=True, timing=True,
ignoredirs=set(sys.path[1:])) if gopts.trace_to_stderr:
result = tracer.runfunc(run) SetTraceToStderr()
else:
result = run() with Trace('starting new command: %s', ', '.join([name] + argv), first_trace=True):
run = lambda: repo._Run(name, gopts, argv) or 0
if gopts.trace_python:
import trace
tracer = trace.Trace(count=False, trace=True, timing=True,
ignoredirs=set(sys.path[1:]))
result = tracer.runfunc(run)
else:
result = run()
except KeyboardInterrupt: except KeyboardInterrupt:
print('aborted by user', file=sys.stderr) print('aborted by user', file=sys.stderr)
result = 1 result = 1

View File

@ -41,7 +41,7 @@ from error import ManifestInvalidRevisionError, ManifestInvalidPathError
from error import NoManifestException, ManifestParseError from error import NoManifestException, ManifestParseError
import platform_utils import platform_utils
import progress import progress
from repo_trace import IsTrace, Trace from repo_trace import Trace
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M, R_WORKTREE_M from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M, R_WORKTREE_M
@ -2416,16 +2416,16 @@ class Project(object):
srcUrl = 'http' + srcUrl[len('persistent-http'):] srcUrl = 'http' + srcUrl[len('persistent-http'):]
cmd += [srcUrl] cmd += [srcUrl]
if IsTrace(): proc = None
Trace('%s', ' '.join(cmd)) with Trace('Fetching bundle: %s', ' '.join(cmd)):
if verbose: if verbose:
print('%s: Downloading bundle: %s' % (self.name, srcUrl)) print('%s: Downloading bundle: %s' % (self.name, srcUrl))
stdout = None if verbose else subprocess.PIPE stdout = None if verbose else subprocess.PIPE
stderr = None if verbose else subprocess.STDOUT stderr = None if verbose else subprocess.STDOUT
try: try:
proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr) proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
except OSError: except OSError:
return False return False
(output, _) = proc.communicate() (output, _) = proc.communicate()
curlret = proc.returncode curlret = proc.returncode

View File

@ -15,26 +15,128 @@
"""Logic for tracing repo interactions. """Logic for tracing repo interactions.
Activated via `repo --trace ...` or `REPO_TRACE=1 repo ...`. Activated via `repo --trace ...` or `REPO_TRACE=1 repo ...`.
Temporary: Tracing is always on. Set `REPO_TRACE=0` to turn off.
To also include trace outputs in stderr do `repo --trace_to_stderr ...`
""" """
import sys import sys
import os import os
import tempfile
import time
from contextlib import ContextDecorator
# Env var to implicitly turn on tracing. # Env var to implicitly turn on tracing.
REPO_TRACE = 'REPO_TRACE' REPO_TRACE = 'REPO_TRACE'
_TRACE = os.environ.get(REPO_TRACE) == '1' # Temporarily set tracing to always on unless user expicitly sets to 0.
_TRACE = os.environ.get(REPO_TRACE) != '0'
_TRACE_TO_STDERR = False
_TRACE_FILE = None
_TRACE_FILE_NAME = 'TRACE_FILE'
_MAX_SIZE = 5 # in mb
_NEW_COMMAND_SEP = '+++++++++++++++NEW COMMAND+++++++++++++++++++'
def IsStraceToStderr():
return _TRACE_TO_STDERR
def IsTrace(): def IsTrace():
return _TRACE return _TRACE
def SetTraceToStderr():
global _TRACE_TO_STDERR
_TRACE_TO_STDERR = True
def SetTrace(): def SetTrace():
global _TRACE global _TRACE
_TRACE = True _TRACE = True
def Trace(fmt, *args): def _SetTraceFile():
if IsTrace(): global _TRACE_FILE
print(fmt % args, file=sys.stderr) _TRACE_FILE = _GetTraceFile()
class Trace(ContextDecorator):
def _time(self):
"""Generate nanoseconds of time in a py3.6 safe way"""
return int(time.time()*1e+9)
def __init__(self, fmt, *args, first_trace=False):
if not IsTrace():
return
self._trace_msg = fmt % args
if not _TRACE_FILE:
_SetTraceFile()
if first_trace:
_ClearOldTraces()
self._trace_msg = '%s %s' % (_NEW_COMMAND_SEP, self._trace_msg)
def __enter__(self):
if not IsTrace():
return self
print_msg = f"PID: {os.getpid()} START: {self._time()} :" + self._trace_msg + '\n'
with open(_TRACE_FILE, 'a') as f:
print(print_msg, file=f)
if _TRACE_TO_STDERR:
print(print_msg, file=sys.stderr)
return self
def __exit__(self, *exc):
if not IsTrace():
return False
print_msg = f"PID: {os.getpid()} END: {self._time()} :" + self._trace_msg + '\n'
with open(_TRACE_FILE, 'a') as f:
print(print_msg, file=f)
if _TRACE_TO_STDERR:
print(print_msg, file=sys.stderr)
return False
def _GetTraceFile():
"""Get the trace file or create one."""
# TODO: refactor to pass repodir to Trace.
repo_dir = os.path.dirname(os.path.dirname(__file__))
trace_file = os.path.join(repo_dir, _TRACE_FILE_NAME)
print('Trace outputs in %s' % trace_file)
return trace_file
def _ClearOldTraces():
"""Clear traces from old commands if trace file is too big.
Note: If the trace file contains output from two `repo`
commands that were running at the same time, this
will not work precisely.
"""
if os.path.isfile(_TRACE_FILE):
while os.path.getsize(_TRACE_FILE)/(1024*1024) > _MAX_SIZE:
temp = tempfile.NamedTemporaryFile(mode='w', delete=False)
with open(_TRACE_FILE, 'r', errors='ignore') as fin:
trace_lines = fin.readlines()
for i , l in enumerate(trace_lines):
if 'END:' in l and _NEW_COMMAND_SEP in l:
temp.writelines(trace_lines[i+1:])
break
temp.close()
os.replace(temp.name, _TRACE_FILE)

View File

@ -20,6 +20,7 @@ import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
import repo_trace
def find_pytest(): def find_pytest():

37
ssh.py
View File

@ -182,28 +182,29 @@ class ProxyManager:
# be important because we can't tell that that 'git@myhost.com' is the same # be important because we can't tell that that 'git@myhost.com' is the same
# as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file. # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file.
check_command = command_base + ['-O', 'check'] check_command = command_base + ['-O', 'check']
try: with Trace('Call to ssh (check call): %s', ' '.join(check_command)):
Trace(': %s', ' '.join(check_command)) try:
check_process = subprocess.Popen(check_command, check_process = subprocess.Popen(check_command,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
check_process.communicate() # read output, but ignore it... check_process.communicate() # read output, but ignore it...
isnt_running = check_process.wait() isnt_running = check_process.wait()
if not isnt_running: if not isnt_running:
# Our double-check found that the master _was_ infact running. Add to # Our double-check found that the master _was_ infact running. Add to
# the list of keys. # the list of keys.
self._master_keys[key] = True self._master_keys[key] = True
return True return True
except Exception: except Exception:
# Ignore excpetions. We we will fall back to the normal command and print # Ignore excpetions. We we will fall back to the normal command and
# to the log there. # print to the log there.
pass pass
command = command_base[:1] + ['-M', '-N'] + command_base[1:] command = command_base[:1] + ['-M', '-N'] + command_base[1:]
p = None
try: try:
Trace(': %s', ' '.join(command)) with Trace('Call to ssh: %s', ' '.join(command)):
p = subprocess.Popen(command) p = subprocess.Popen(command)
except Exception as e: except Exception as e:
self._master_broken.value = True self._master_broken.value = True
print('\nwarn: cannot enable ssh control master for %s:%s\n%s' print('\nwarn: cannot enable ssh control master for %s:%s\n%s'

View File

@ -60,7 +60,7 @@ from error import RepoChangedException, GitError, ManifestParseError
import platform_utils import platform_utils
from project import SyncBuffer from project import SyncBuffer
from progress import Progress from progress import Progress
from repo_trace import IsTrace, Trace from repo_trace import Trace
import ssh import ssh
from wrapper import Wrapper from wrapper import Wrapper
from manifest_xml import GitcManifest from manifest_xml import GitcManifest
@ -739,7 +739,6 @@ later is required to fix a server side protocol bug.
bak_dir = os.path.join(objdir, '.repo', 'pack.bak') bak_dir = os.path.join(objdir, '.repo', 'pack.bak')
if not _BACKUP_OBJECTS or not platform_utils.isdir(pack_dir): if not _BACKUP_OBJECTS or not platform_utils.isdir(pack_dir):
return return
saved = []
files = set(platform_utils.listdir(pack_dir)) files = set(platform_utils.listdir(pack_dir))
to_backup = [] to_backup = []
for f in files: for f in files:
@ -751,12 +750,10 @@ later is required to fix a server side protocol bug.
for fname in to_backup: for fname in to_backup:
bak_fname = os.path.join(bak_dir, fname) bak_fname = os.path.join(bak_dir, fname)
if not os.path.exists(bak_fname): if not os.path.exists(bak_fname):
saved.append(fname) with Trace('%s saved %s', bare_git._project.name, fname):
# Use a tmp file so that we are sure of a complete copy. # Use a tmp file so that we are sure of a complete copy.
shutil.copy(os.path.join(pack_dir, fname), bak_fname + '.tmp') shutil.copy(os.path.join(pack_dir, fname), bak_fname + '.tmp')
shutil.move(bak_fname + '.tmp', bak_fname) shutil.move(bak_fname + '.tmp', bak_fname)
if saved:
Trace('%s saved %s', bare_git._project.name, ' '.join(saved))
def _GCProjects(self, projects, opt, err_event): def _GCProjects(self, projects, opt, err_event):
pm = Progress('Garbage collecting', len(projects), delay=False, quiet=opt.quiet) pm = Progress('Garbage collecting', len(projects), delay=False, quiet=opt.quiet)

View File

@ -19,6 +19,7 @@ import tempfile
import unittest import unittest
import git_config import git_config
import repo_trace
def fixture(*paths): def fixture(*paths):
@ -33,9 +34,16 @@ class GitConfigReadOnlyTests(unittest.TestCase):
def setUp(self): def setUp(self):
"""Create a GitConfig object using the test.gitconfig fixture. """Create a GitConfig object using the test.gitconfig fixture.
""" """
self.tempdirobj = tempfile.TemporaryDirectory(prefix='repo_tests')
repo_trace._TRACE_FILE = os.path.join(self.tempdirobj.name, 'TRACE_FILE_from_test')
config_fixture = fixture('test.gitconfig') config_fixture = fixture('test.gitconfig')
self.config = git_config.GitConfig(config_fixture) self.config = git_config.GitConfig(config_fixture)
def tearDown(self):
self.tempdirobj.cleanup()
def test_GetString_with_empty_config_values(self): def test_GetString_with_empty_config_values(self):
""" """
Test config entries with no value. Test config entries with no value.
@ -109,9 +117,15 @@ class GitConfigReadWriteTests(unittest.TestCase):
"""Read/write tests of the GitConfig class.""" """Read/write tests of the GitConfig class."""
def setUp(self): def setUp(self):
self.tempdirobj = tempfile.TemporaryDirectory(prefix='repo_tests')
repo_trace._TRACE_FILE = os.path.join(self.tempdirobj.name, 'TRACE_FILE_from_test')
self.tmpfile = tempfile.NamedTemporaryFile() self.tmpfile = tempfile.NamedTemporaryFile()
self.config = self.get_config() self.config = self.get_config()
def tearDown(self):
self.tempdirobj.cleanup()
def get_config(self): def get_config(self):
"""Get a new GitConfig instance.""" """Get a new GitConfig instance."""
return git_config.GitConfig(self.tmpfile.name) return git_config.GitConfig(self.tmpfile.name)

View File

@ -24,6 +24,7 @@ from unittest import mock
import git_superproject import git_superproject
import git_trace2_event_log import git_trace2_event_log
import manifest_xml import manifest_xml
import repo_trace
from test_manifest_xml import sort_attributes from test_manifest_xml import sort_attributes
@ -39,6 +40,7 @@ class SuperprojectTestCase(unittest.TestCase):
"""Set up superproject every time.""" """Set up superproject every time."""
self.tempdirobj = tempfile.TemporaryDirectory(prefix='repo_tests') self.tempdirobj = tempfile.TemporaryDirectory(prefix='repo_tests')
self.tempdir = self.tempdirobj.name self.tempdir = self.tempdirobj.name
repo_trace._TRACE_FILE = os.path.join(self.tempdir, 'TRACE_FILE_from_test')
self.repodir = os.path.join(self.tempdir, '.repo') self.repodir = os.path.join(self.tempdir, '.repo')
self.manifest_file = os.path.join( self.manifest_file = os.path.join(
self.repodir, manifest_xml.MANIFEST_FILE_NAME) self.repodir, manifest_xml.MANIFEST_FILE_NAME)

View File

@ -23,6 +23,7 @@ import xml.dom.minidom
import error import error
import manifest_xml import manifest_xml
import repo_trace
# Invalid paths that we don't want in the filesystem. # Invalid paths that we don't want in the filesystem.
@ -93,6 +94,7 @@ class ManifestParseTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.tempdirobj = tempfile.TemporaryDirectory(prefix='repo_tests') self.tempdirobj = tempfile.TemporaryDirectory(prefix='repo_tests')
self.tempdir = self.tempdirobj.name self.tempdir = self.tempdirobj.name
repo_trace._TRACE_FILE = os.path.join(self.tempdir, 'TRACE_FILE_from_test')
self.repodir = os.path.join(self.tempdir, '.repo') self.repodir = os.path.join(self.tempdir, '.repo')
self.manifest_dir = os.path.join(self.repodir, 'manifests') self.manifest_dir = os.path.join(self.repodir, 'manifests')
self.manifest_file = os.path.join( self.manifest_file = os.path.join(

View File

@ -26,6 +26,7 @@ import git_command
import git_config import git_config
import platform_utils import platform_utils
import project import project
import repo_trace
@contextlib.contextmanager @contextlib.contextmanager
@ -64,6 +65,13 @@ class FakeProject(object):
class ReviewableBranchTests(unittest.TestCase): class ReviewableBranchTests(unittest.TestCase):
"""Check ReviewableBranch behavior.""" """Check ReviewableBranch behavior."""
def setUp(self):
self.tempdirobj = tempfile.TemporaryDirectory(prefix='repo_tests')
repo_trace._TRACE_FILE = os.path.join(self.tempdirobj.name, 'TRACE_FILE_from_test')
def tearDown(self):
self.tempdirobj.cleanup()
def test_smoke(self): def test_smoke(self):
"""A quick run through everything.""" """A quick run through everything."""
with TempGitTree() as tempdir: with TempGitTree() as tempdir: