Skip to content

Commit

Permalink
Merge pull request #475 from jkloetzke/win32-robust-rename
Browse files Browse the repository at this point in the history
Fix spurious file renaming errors on Windows
  • Loading branch information
jkloetzke authored Jun 8, 2022
2 parents 0a1c317 + e166b6b commit 0e9bfbe
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 11 deletions.
4 changes: 2 additions & 2 deletions pym/bob/generators/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later

from ..errors import BuildError
from ..utils import removePath, isWindows, summonMagic
from ..utils import removePath, isWindows, summonMagic, replacePath
import argparse
import os
import re
Expand Down Expand Up @@ -346,7 +346,7 @@ def updateFile(self, name, content, encoding=None, newline=None):
oldContent = None

if oldContent != newContent:
os.replace(newName, name)
replacePath(newName, name)
with open(oldName, "wb") as f:
f.write(newContent)
else:
Expand Down
5 changes: 3 additions & 2 deletions pym/bob/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from .stringparser import checkGlobList, Env, DEFAULT_STRING_FUNS, IfExpression
from .tty import InfoOnce, Warn, WarnOnce, setColorMode
from .utils import asHexStr, joinScripts, compareVersion, binStat, \
updateDicRecursive, hashString, getPlatformTag, getPlatformString
updateDicRecursive, hashString, getPlatformTag, getPlatformString, \
replacePath
from itertools import chain
from os.path import expanduser
from string import Template
Expand Down Expand Up @@ -3859,7 +3860,7 @@ def __generatePackages(self, nameFormatter, cacheKey, sandboxEnabled):
with open(newCacheName, "wb") as f:
f.write(cacheKey)
PackagePickler(f, nameFormatter).dump(result)
os.replace(newCacheName, cacheName)
replacePath(newCacheName, cacheName)
except OSError as e:
print("Error saving internal state:", str(e), file=sys.stderr)

Expand Down
5 changes: 3 additions & 2 deletions pym/bob/scm/url.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from .. import BOB_VERSION
from ..errors import BuildError, ParseError
from ..stringparser import IfExpression
from ..utils import asHexStr, hashFile, isWindows, removeUserFromUrl, sslNoVerifyContext
from ..utils import asHexStr, hashFile, isWindows, removeUserFromUrl, sslNoVerifyContext, \
replacePath
from .scm import Scm, ScmAudit
from http.client import HTTPException
import asyncio
Expand Down Expand Up @@ -252,7 +253,7 @@ def _download(self, destination):
# Atomically move file to destination. Set explicit mode to
# retain Bob 0.15 behaviour.
os.chmod(tmpFileName, stat.S_IREAD|stat.S_IWRITE)
os.replace(tmpFileName, destination)
replacePath(tmpFileName, destination)
tmpFileName = None

except urllib.error.HTTPError as e:
Expand Down
5 changes: 3 additions & 2 deletions pym/bob/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from .errors import ParseError
from .tty import colorize, WarnOnce, WARNING
from .utils import replacePath
import copy
import errno
import os
Expand Down Expand Up @@ -169,7 +170,7 @@ def __save(self):
with open(dirtyPath, "wb") as f:
with DigestAdder(f) as df:
pickle.dump(state, df)
os.replace(dirtyPath, self.__uncommittedPath)
replacePath(dirtyPath, self.__uncommittedPath)
except OSError as e:
raise ParseError("Error saving workspace state: " + str(e))
else:
Expand All @@ -188,7 +189,7 @@ def __commit(self, verify=True):
commit = (csum == data[-4:])
os.fsync(f.fileno())
if commit:
os.replace(self.__uncommittedPath, self.__path)
replacePath(self.__uncommittedPath, self.__path)
return
else:
print(colorize("Warning: discarding corrupted workspace state!", WARNING),
Expand Down
17 changes: 16 additions & 1 deletion pym/bob/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,25 @@ def isWindows():
else:
__isWindows = True

def _replacePathWin32(src, dst):
# Workaround for spurious PermissionError's on Windows.
i = 0
while True:
try:
os.replace(src, dst)
break
except PermissionError:
if i >= 10: raise
import time
time.sleep(0.1 * i)
i += 1

if __isWindows:
INVALID_CHAR_TRANS = str.maketrans(':*?<>"|', '_______')
replacePath = _replacePathWin32
else:
INVALID_CHAR_TRANS = str.maketrans('', '')
replacePath = os.replace


__canSymlink = None
Expand Down Expand Up @@ -352,7 +367,7 @@ def close(self):
self.__inFile.close()
if self.__outFile:
self.__outFile.close()
os.replace(self.__outFile.name, self.__cachePath)
replacePath(self.__outFile.name, self.__cachePath)
except OSError as e:
raise BuildError("Error closing hash cache: " + str(e))

Expand Down
27 changes: 25 additions & 2 deletions test/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@

from tempfile import NamedTemporaryFile, TemporaryDirectory
from unittest import TestCase
from unittest.mock import patch
from unittest.mock import patch, MagicMock
import os, stat
import asyncio

from bob.utils import joinScripts, removePath, emptyDirectory, compareVersion, \
getPlatformTag, run, check_output, removeUserFromUrl, runInEventLoop
getPlatformTag, run, check_output, removeUserFromUrl, runInEventLoop, \
_replacePathWin32
from bob.errors import BuildError, ParseError

class TestJoinScripts(TestCase):
Expand Down Expand Up @@ -295,3 +296,25 @@ def testWinUncFileUrl(self):
self.assertEqual(removeUserFromUrl(r"file:///\\server\path"),
r"file:///\\server\path")

class TestReplacePath(TestCase):
def testWin32Ok(self):
m = MagicMock(return_value=None)
with patch('os.replace', m):
_replacePathWin32("foo", "bar")
m.assert_called_with("foo", "bar")

def testWin32Spurious(self):
"""One PermissionError is discarded"""
m = MagicMock(side_effect=[PermissionError(42), None])
with patch('os.replace', m):
_replacePathWin32("foo", "bar")
m.assert_called_with("foo", "bar")

@patch('time.sleep', MagicMock())
def testWin32Fail(self):
"""Sticky PermissionError is still raised"""
m = MagicMock(side_effect=PermissionError(42))
with self.assertRaises(PermissionError):
with patch('os.replace', m):
_replacePathWin32("foo", "bar")
m.assert_called_with("foo", "bar")

0 comments on commit 0e9bfbe

Please sign in to comment.