diff options
| author | Joanna Wang <jojwang@google.com> | 2022-12-09 17:49:07 -0500 |
|---|---|---|
| committer | Joanna Wang <jojwang@google.com> | 2022-12-09 22:49:31 +0000 |
| commit | 0324e43242ff078dfce70e80ce8e00d394f5ccdb (patch) | |
| tree | d9a08362186827c10c8fbe01162692c9ac1a575a | |
| parent | 8d25584f6987bbef81277996203f0967c4d8b4da (diff) | |
| download | git-repo-0324e43242ff078dfce70e80ce8e00d394f5ccdb.tar.gz | |
repo_trace: Avoid race conditions with trace_file updating.
Change-Id: I0bc1bb3c8f60465dc6bee5081688a9f163dd8cf8
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/354515
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Joanna Wang <jojwang@google.com>
| -rw-r--r-- | repo_trace.py | 55 | ||||
| -rw-r--r-- | tests/test_repo_trace.py | 56 |
2 files changed, 93 insertions, 18 deletions
diff --git a/repo_trace.py b/repo_trace.py index d79408d9..1ba86c79 100644 --- a/repo_trace.py +++ b/repo_trace.py | |||
| @@ -23,6 +23,7 @@ To also include trace outputs in stderr do `repo --trace_to_stderr ...` | |||
| 23 | import sys | 23 | import sys |
| 24 | import os | 24 | import os |
| 25 | import time | 25 | import time |
| 26 | import tempfile | ||
| 26 | from contextlib import ContextDecorator | 27 | from contextlib import ContextDecorator |
| 27 | 28 | ||
| 28 | import platform_utils | 29 | import platform_utils |
| @@ -35,34 +36,40 @@ _TRACE = os.environ.get(REPO_TRACE) != '0' | |||
| 35 | _TRACE_TO_STDERR = False | 36 | _TRACE_TO_STDERR = False |
| 36 | _TRACE_FILE = None | 37 | _TRACE_FILE = None |
| 37 | _TRACE_FILE_NAME = 'TRACE_FILE' | 38 | _TRACE_FILE_NAME = 'TRACE_FILE' |
| 38 | _MAX_SIZE = 70 # in mb | 39 | _MAX_SIZE = 70 # in MiB |
| 39 | _NEW_COMMAND_SEP = '+++++++++++++++NEW COMMAND+++++++++++++++++++' | 40 | _NEW_COMMAND_SEP = '+++++++++++++++NEW COMMAND+++++++++++++++++++' |
| 40 | 41 | ||
| 41 | 42 | ||
| 42 | def IsTraceToStderr(): | 43 | def IsTraceToStderr(): |
| 44 | """Whether traces are written to stderr.""" | ||
| 43 | return _TRACE_TO_STDERR | 45 | return _TRACE_TO_STDERR |
| 44 | 46 | ||
| 45 | 47 | ||
| 46 | def IsTrace(): | 48 | def IsTrace(): |
| 49 | """Whether tracing is enabled.""" | ||
| 47 | return _TRACE | 50 | return _TRACE |
| 48 | 51 | ||
| 49 | 52 | ||
| 50 | def SetTraceToStderr(): | 53 | def SetTraceToStderr(): |
| 54 | """Enables tracing logging to stderr.""" | ||
| 51 | global _TRACE_TO_STDERR | 55 | global _TRACE_TO_STDERR |
| 52 | _TRACE_TO_STDERR = True | 56 | _TRACE_TO_STDERR = True |
| 53 | 57 | ||
| 54 | 58 | ||
| 55 | def SetTrace(): | 59 | def SetTrace(): |
| 60 | """Enables tracing.""" | ||
| 56 | global _TRACE | 61 | global _TRACE |
| 57 | _TRACE = True | 62 | _TRACE = True |
| 58 | 63 | ||
| 59 | 64 | ||
| 60 | def _SetTraceFile(quiet): | 65 | def _SetTraceFile(quiet): |
| 66 | """Sets the trace file location.""" | ||
| 61 | global _TRACE_FILE | 67 | global _TRACE_FILE |
| 62 | _TRACE_FILE = _GetTraceFile(quiet) | 68 | _TRACE_FILE = _GetTraceFile(quiet) |
| 63 | 69 | ||
| 64 | 70 | ||
| 65 | class Trace(ContextDecorator): | 71 | class Trace(ContextDecorator): |
| 72 | """Used to capture and save git traces.""" | ||
| 66 | 73 | ||
| 67 | def _time(self): | 74 | def _time(self): |
| 68 | """Generate nanoseconds of time in a py3.6 safe way""" | 75 | """Generate nanoseconds of time in a py3.6 safe way""" |
| @@ -128,20 +135,32 @@ def _GetTraceFile(quiet): | |||
| 128 | 135 | ||
| 129 | 136 | ||
| 130 | def _ClearOldTraces(): | 137 | def _ClearOldTraces(): |
| 131 | """Clear the oldest commands if trace file is too big. | 138 | """Clear the oldest commands if trace file is too big.""" |
| 132 | 139 | try: | |
| 133 | Note: If the trace file contains output from two `repo` | 140 | with open(_TRACE_FILE, 'r', errors='ignore') as f: |
| 134 | commands that were running at the same time, this | 141 | if os.path.getsize(f.name) / (1024 * 1024) <= _MAX_SIZE: |
| 135 | will not work precisely. | 142 | return |
| 136 | """ | 143 | trace_lines = f.readlines() |
| 137 | if os.path.isfile(_TRACE_FILE): | 144 | except FileNotFoundError: |
| 138 | while os.path.getsize(_TRACE_FILE) / (1024 * 1024) > _MAX_SIZE: | 145 | return |
| 139 | temp_file = _TRACE_FILE + '.tmp' | 146 | |
| 140 | with open(_TRACE_FILE, 'r', errors='ignore') as fin: | 147 | while sum(len(x) for x in trace_lines) / (1024 * 1024) > _MAX_SIZE: |
| 141 | with open(temp_file, 'w') as tf: | 148 | for i, line in enumerate(trace_lines): |
| 142 | trace_lines = fin.readlines() | 149 | if 'END:' in line and _NEW_COMMAND_SEP in line: |
| 143 | for i, l in enumerate(trace_lines): | 150 | trace_lines = trace_lines[i + 1:] |
| 144 | if 'END:' in l and _NEW_COMMAND_SEP in l: | 151 | break |
| 145 | tf.writelines(trace_lines[i + 1:]) | 152 | else: |
| 146 | break | 153 | # The last chunk is bigger than _MAX_SIZE, so just throw everything away. |
| 147 | platform_utils.rename(temp_file, _TRACE_FILE) | 154 | trace_lines = [] |
| 155 | |||
| 156 | while trace_lines and trace_lines[-1] == '\n': | ||
| 157 | trace_lines = trace_lines[:-1] | ||
| 158 | # Write to a temporary file with a unique name in the same filesystem | ||
| 159 | # before replacing the original trace file. | ||
| 160 | temp_dir, temp_prefix = os.path.split(_TRACE_FILE) | ||
| 161 | with tempfile.NamedTemporaryFile('w', | ||
| 162 | dir=temp_dir, | ||
| 163 | prefix=temp_prefix, | ||
| 164 | delete=False) as f: | ||
| 165 | f.writelines(trace_lines) | ||
| 166 | platform_utils.rename(f.name, _TRACE_FILE) | ||
diff --git a/tests/test_repo_trace.py b/tests/test_repo_trace.py new file mode 100644 index 00000000..5faf2938 --- /dev/null +++ b/tests/test_repo_trace.py | |||
| @@ -0,0 +1,56 @@ | |||
| 1 | # Copyright 2022 The Android Open Source Project | ||
| 2 | # | ||
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | # you may not use this file except in compliance with the License. | ||
| 5 | # You may obtain a copy of the License at | ||
| 6 | # | ||
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | # | ||
| 9 | # Unless required by applicable law or agreed to in writing, software | ||
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | # See the License for the specific language governing permissions and | ||
| 13 | # limitations under the License. | ||
| 14 | |||
| 15 | """Unittests for the repo_trace.py module.""" | ||
| 16 | |||
| 17 | import os | ||
| 18 | import unittest | ||
| 19 | from unittest import mock | ||
| 20 | |||
| 21 | import repo_trace | ||
| 22 | |||
| 23 | |||
| 24 | class TraceTests(unittest.TestCase): | ||
| 25 | """Check Trace behavior.""" | ||
| 26 | |||
| 27 | def testTrace_MaxSizeEnforced(self): | ||
| 28 | content = 'git chicken' | ||
| 29 | |||
| 30 | with repo_trace.Trace(content, first_trace=True): | ||
| 31 | pass | ||
| 32 | first_trace_size = os.path.getsize(repo_trace._TRACE_FILE) | ||
| 33 | |||
| 34 | with repo_trace.Trace(content): | ||
| 35 | pass | ||
| 36 | self.assertGreater( | ||
| 37 | os.path.getsize(repo_trace._TRACE_FILE), first_trace_size) | ||
| 38 | |||
| 39 | # Check we clear everything is the last chunk is larger than _MAX_SIZE. | ||
| 40 | with mock.patch('repo_trace._MAX_SIZE', 0): | ||
| 41 | with repo_trace.Trace(content, first_trace=True): | ||
| 42 | pass | ||
| 43 | self.assertEqual(first_trace_size, | ||
| 44 | os.path.getsize(repo_trace._TRACE_FILE)) | ||
| 45 | |||
| 46 | # Check we only clear the chunks we need to. | ||
| 47 | repo_trace._MAX_SIZE = (first_trace_size + 1) / (1024 * 1024) | ||
| 48 | with repo_trace.Trace(content, first_trace=True): | ||
| 49 | pass | ||
| 50 | self.assertEqual(first_trace_size * 2, | ||
| 51 | os.path.getsize(repo_trace._TRACE_FILE)) | ||
| 52 | |||
| 53 | with repo_trace.Trace(content, first_trace=True): | ||
| 54 | pass | ||
| 55 | self.assertEqual(first_trace_size * 2, | ||
| 56 | os.path.getsize(repo_trace._TRACE_FILE)) | ||
