| # Copyright (C) 2008 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Logic for tracing repo interactions. |
| |
| 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 contextlib |
| import os |
| import sys |
| import tempfile |
| import time |
| |
| import platform_utils |
| |
| |
| # Env var to implicitly turn on tracing. |
| REPO_TRACE = "REPO_TRACE" |
| |
| # 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 = 70 # in MiB |
| _NEW_COMMAND_SEP = "+++++++++++++++NEW COMMAND+++++++++++++++++++" |
| |
| |
| def IsTraceToStderr(): |
| """Whether traces are written to stderr.""" |
| return _TRACE_TO_STDERR |
| |
| |
| def IsTrace(): |
| """Whether tracing is enabled.""" |
| return _TRACE |
| |
| |
| def SetTraceToStderr(): |
| """Enables tracing logging to stderr.""" |
| global _TRACE_TO_STDERR |
| _TRACE_TO_STDERR = True |
| |
| |
| def SetTrace(): |
| """Enables tracing.""" |
| global _TRACE |
| _TRACE = True |
| |
| |
| def _SetTraceFile(quiet): |
| """Sets the trace file location.""" |
| global _TRACE_FILE |
| _TRACE_FILE = _GetTraceFile(quiet) |
| |
| |
| class Trace(contextlib.ContextDecorator): |
| """Used to capture and save git traces.""" |
| |
| def _time(self): |
| """Generate nanoseconds of time in a py3.6 safe way""" |
| return int(time.time() * 1e9) |
| |
| def __init__(self, fmt, *args, first_trace=False, quiet=True): |
| """Initialize the object. |
| |
| Args: |
| fmt: The format string for the trace. |
| *args: Arguments to pass to formatting. |
| first_trace: Whether this is the first trace of a `repo` invocation. |
| quiet: Whether to suppress notification of trace file location. |
| """ |
| if not IsTrace(): |
| return |
| self._trace_msg = fmt % args |
| |
| if not _TRACE_FILE: |
| _SetTraceFile(quiet) |
| |
| if first_trace: |
| _ClearOldTraces() |
| self._trace_msg = f"{_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(quiet): |
| """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) |
| if not quiet: |
| print(f"Trace outputs in {trace_file}", file=sys.stderr) |
| return trace_file |
| |
| |
| def _ClearOldTraces(): |
| """Clear the oldest commands if trace file is too big.""" |
| try: |
| with open(_TRACE_FILE, errors="ignore") as f: |
| if os.path.getsize(f.name) / (1024 * 1024) <= _MAX_SIZE: |
| return |
| trace_lines = f.readlines() |
| except FileNotFoundError: |
| return |
| |
| while sum(len(x) for x in trace_lines) / (1024 * 1024) > _MAX_SIZE: |
| for i, line in enumerate(trace_lines): |
| if "END:" in line and _NEW_COMMAND_SEP in line: |
| trace_lines = trace_lines[i + 1 :] |
| break |
| else: |
| # The last chunk is bigger than _MAX_SIZE, so just throw everything |
| # away. |
| trace_lines = [] |
| |
| while trace_lines and trace_lines[-1] == "\n": |
| trace_lines = trace_lines[:-1] |
| # Write to a temporary file with a unique name in the same filesystem |
| # before replacing the original trace file. |
| temp_dir, temp_prefix = os.path.split(_TRACE_FILE) |
| with tempfile.NamedTemporaryFile( |
| "w", dir=temp_dir, prefix=temp_prefix, delete=False |
| ) as f: |
| f.writelines(trace_lines) |
| platform_utils.rename(f.name, _TRACE_FILE) |