summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Frysinger <vapier@google.com>2021-09-28 11:27:24 -0400
committerMike Frysinger <vapier@google.com>2021-09-28 16:06:50 +0000
commit9d96f58f5fcec101c612e61c3e2526ca071d89ea (patch)
tree63bc9e73e3b7d74a2cf5352239bf4f2e9695b507
parent7a1e7e772f3bbc67660e824c98f527b5f608ac24 (diff)
downloadgit-repo-9d96f58f5fcec101c612e61c3e2526ca071d89ea.tar.gz
make file removal a bit more robust
Some of the file removal calls are subject to race conditions (if something else deletes the file), so extend our remove API to have an option to ignore ENOENT errors. Then update a bunch of random call sites to use this new functionality. Change-Id: I31a9090e135452033135337a202a4fc2dbf8b63c Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/319195 Reviewed-by: Sean McAllister <smcallis@google.com> Tested-by: Mike Frysinger <vapier@google.com>
-rw-r--r--git_config.py7
-rw-r--r--manifest_xml.py3
-rw-r--r--platform_utils.py31
-rw-r--r--project.py20
-rw-r--r--subcmds/sync.py20
-rw-r--r--tests/test_platform_utils.py50
6 files changed, 80 insertions, 51 deletions
diff --git a/git_config.py b/git_config.py
index 778e81a4..bc70d160 100644
--- a/git_config.py
+++ b/git_config.py
@@ -352,8 +352,8 @@ class GitConfig(object):
352 Trace(': parsing %s', self.file) 352 Trace(': parsing %s', self.file)
353 with open(self._json) as fd: 353 with open(self._json) as fd:
354 return json.load(fd) 354 return json.load(fd)
355 except (IOError, ValueError): 355 except (IOError, ValueErrorl):
356 platform_utils.remove(self._json) 356 platform_utils.remove(self._json, missing_ok=True)
357 return None 357 return None
358 358
359 def _SaveJson(self, cache): 359 def _SaveJson(self, cache):
@@ -361,8 +361,7 @@ class GitConfig(object):
361 with open(self._json, 'w') as fd: 361 with open(self._json, 'w') as fd:
362 json.dump(cache, fd, indent=2) 362 json.dump(cache, fd, indent=2)
363 except (IOError, TypeError): 363 except (IOError, TypeError):
364 if os.path.exists(self._json): 364 platform_utils.remove(self._json, missing_ok=True)
365 platform_utils.remove(self._json)
366 365
367 def _ReadGit(self): 366 def _ReadGit(self):
368 """ 367 """
diff --git a/manifest_xml.py b/manifest_xml.py
index 135c91fb..86f20202 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -270,8 +270,7 @@ class XmlManifest(object):
270 self.Override(name) 270 self.Override(name)
271 271
272 # Old versions of repo would generate symlinks we need to clean up. 272 # Old versions of repo would generate symlinks we need to clean up.
273 if os.path.lexists(self.manifestFile): 273 platform_utils.remove(self.manifestFile, missing_ok=True)
274 platform_utils.remove(self.manifestFile)
275 # This file is interpreted as if it existed inside the manifest repo. 274 # This file is interpreted as if it existed inside the manifest repo.
276 # That allows us to use <include> with the relative file name. 275 # That allows us to use <include> with the relative file name.
277 with open(self.manifestFile, 'w') as fp: 276 with open(self.manifestFile, 'w') as fp:
diff --git a/platform_utils.py b/platform_utils.py
index 5741f4d3..0203249a 100644
--- a/platform_utils.py
+++ b/platform_utils.py
@@ -127,28 +127,27 @@ def rename(src, dst):
127 shutil.move(src, dst) 127 shutil.move(src, dst)
128 128
129 129
130def remove(path): 130def remove(path, missing_ok=False):
131 """Remove (delete) the file path. This is a replacement for os.remove that 131 """Remove (delete) the file path. This is a replacement for os.remove that
132 allows deleting read-only files on Windows, with support for long paths and 132 allows deleting read-only files on Windows, with support for long paths and
133 for deleting directory symbolic links. 133 for deleting directory symbolic links.
134 134
135 Availability: Unix, Windows.""" 135 Availability: Unix, Windows."""
136 if isWindows(): 136 longpath = _makelongpath(path) if isWindows() else path
137 longpath = _makelongpath(path) 137 try:
138 try: 138 os.remove(longpath)
139 os.remove(longpath) 139 except OSError as e:
140 except OSError as e: 140 if e.errno == errno.EACCES:
141 if e.errno == errno.EACCES: 141 os.chmod(longpath, stat.S_IWRITE)
142 os.chmod(longpath, stat.S_IWRITE) 142 # Directory symbolic links must be deleted with 'rmdir'.
143 # Directory symbolic links must be deleted with 'rmdir'. 143 if islink(longpath) and isdir(longpath):
144 if islink(longpath) and isdir(longpath): 144 os.rmdir(longpath)
145 os.rmdir(longpath)
146 else:
147 os.remove(longpath)
148 else: 145 else:
149 raise 146 os.remove(longpath)
150 else: 147 elif missing_ok and e.errno == errno.ENOENT:
151 os.remove(path) 148 pass
149 else:
150 raise
152 151
153 152
154def walk(top, topdown=True, onerror=None, followlinks=False): 153def walk(top, topdown=True, onerror=None, followlinks=False):
diff --git a/project.py b/project.py
index fe88a505..634d88c5 100644
--- a/project.py
+++ b/project.py
@@ -1182,10 +1182,8 @@ class Project(object):
1182 self._InitMRef() 1182 self._InitMRef()
1183 else: 1183 else:
1184 self._InitMirrorHead() 1184 self._InitMirrorHead()
1185 try: 1185 platform_utils.remove(os.path.join(self.gitdir, 'FETCH_HEAD'),
1186 platform_utils.remove(os.path.join(self.gitdir, 'FETCH_HEAD')) 1186 missing_ok=True)
1187 except OSError:
1188 pass
1189 return True 1187 return True
1190 1188
1191 def PostRepoUpgrade(self): 1189 def PostRepoUpgrade(self):
@@ -2307,15 +2305,12 @@ class Project(object):
2307 cmd.append('+refs/tags/*:refs/tags/*') 2305 cmd.append('+refs/tags/*:refs/tags/*')
2308 2306
2309 ok = GitCommand(self, cmd, bare=True).Wait() == 0 2307 ok = GitCommand(self, cmd, bare=True).Wait() == 0
2310 if os.path.exists(bundle_dst): 2308 platform_utils.remove(bundle_dst, missing_ok=True)
2311 platform_utils.remove(bundle_dst) 2309 platform_utils.remove(bundle_tmp, missing_ok=True)
2312 if os.path.exists(bundle_tmp):
2313 platform_utils.remove(bundle_tmp)
2314 return ok 2310 return ok
2315 2311
2316 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose): 2312 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
2317 if os.path.exists(dstPath): 2313 platform_utils.remove(dstPath, missing_ok=True)
2318 platform_utils.remove(dstPath)
2319 2314
2320 cmd = ['curl', '--fail', '--output', tmpPath, '--netrc', '--location'] 2315 cmd = ['curl', '--fail', '--output', tmpPath, '--netrc', '--location']
2321 if quiet: 2316 if quiet:
@@ -2739,10 +2734,7 @@ class Project(object):
2739 # If the source file doesn't exist, ensure the destination 2734 # If the source file doesn't exist, ensure the destination
2740 # file doesn't either. 2735 # file doesn't either.
2741 if name in symlink_files and not os.path.lexists(src): 2736 if name in symlink_files and not os.path.lexists(src):
2742 try: 2737 platform_utils.remove(dst, missing_ok=True)
2743 platform_utils.remove(dst)
2744 except OSError:
2745 pass
2746 2738
2747 except OSError as e: 2739 except OSError as e:
2748 if e.errno == errno.EPERM: 2740 if e.errno == errno.EPERM:
diff --git a/subcmds/sync.py b/subcmds/sync.py
index c99b06ca..3211cbb1 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -767,13 +767,9 @@ later is required to fix a server side protocol bug.
767 set(new_copyfile_paths)) 767 set(new_copyfile_paths))
768 768
769 for need_remove_file in need_remove_files: 769 for need_remove_file in need_remove_files:
770 try: 770 # Try to remove the updated copyfile or linkfile.
771 platform_utils.remove(need_remove_file) 771 # So, if the file is not exist, nothing need to do.
772 except OSError as e: 772 platform_utils.remove(need_remove_file, missing_ok=True)
773 if e.errno == errno.ENOENT:
774 # Try to remove the updated copyfile or linkfile.
775 # So, if the file is not exist, nothing need to do.
776 pass
777 773
778 # Create copy-link-files.json, save dest path of "copyfile" and "linkfile". 774 # Create copy-link-files.json, save dest path of "copyfile" and "linkfile".
779 with open(copylinkfile_path, 'w', encoding='utf-8') as fp: 775 with open(copylinkfile_path, 'w', encoding='utf-8') as fp:
@@ -1171,10 +1167,7 @@ class _FetchTimes(object):
1171 with open(self._path) as f: 1167 with open(self._path) as f:
1172 self._times = json.load(f) 1168 self._times = json.load(f)
1173 except (IOError, ValueError): 1169 except (IOError, ValueError):
1174 try: 1170 platform_utils.remove(self._path, missing_ok=True)
1175 platform_utils.remove(self._path)
1176 except OSError:
1177 pass
1178 self._times = {} 1171 self._times = {}
1179 1172
1180 def Save(self): 1173 def Save(self):
@@ -1192,10 +1185,7 @@ class _FetchTimes(object):
1192 with open(self._path, 'w') as f: 1185 with open(self._path, 'w') as f:
1193 json.dump(self._times, f, indent=2) 1186 json.dump(self._times, f, indent=2)
1194 except (IOError, TypeError): 1187 except (IOError, TypeError):
1195 try: 1188 platform_utils.remove(self._path, missing_ok=True)
1196 platform_utils.remove(self._path)
1197 except OSError:
1198 pass
1199 1189
1200# This is a replacement for xmlrpc.client.Transport using urllib2 1190# This is a replacement for xmlrpc.client.Transport using urllib2
1201# and supporting persistent-http[s]. It cannot change hosts from 1191# and supporting persistent-http[s]. It cannot change hosts from
diff --git a/tests/test_platform_utils.py b/tests/test_platform_utils.py
new file mode 100644
index 00000000..55b7805c
--- /dev/null
+++ b/tests/test_platform_utils.py
@@ -0,0 +1,50 @@
1# Copyright 2021 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 platform_utils.py module."""
16
17import os
18import tempfile
19import unittest
20
21import platform_utils
22
23
24class RemoveTests(unittest.TestCase):
25 """Check remove() helper."""
26
27 def testMissingOk(self):
28 """Check missing_ok handling."""
29 with tempfile.TemporaryDirectory() as tmpdir:
30 path = os.path.join(tmpdir, 'test')
31
32 # Should not fail.
33 platform_utils.remove(path, missing_ok=True)
34
35 # Should fail.
36 self.assertRaises(OSError, platform_utils.remove, path)
37 self.assertRaises(OSError, platform_utils.remove, path, missing_ok=False)
38
39 # Should not fail if it exists.
40 open(path, 'w').close()
41 platform_utils.remove(path, missing_ok=True)
42 self.assertFalse(os.path.exists(path))
43
44 open(path, 'w').close()
45 platform_utils.remove(path)
46 self.assertFalse(os.path.exists(path))
47
48 open(path, 'w').close()
49 platform_utils.remove(path, missing_ok=False)
50 self.assertFalse(os.path.exists(path))