Compare commits

...

3 Commits

Author SHA1 Message Date
Emanuel Hoogeveen
45806d44a7
[downloader] Obey --file-access-retries when deleting/renaming (#2224)
Authored by: ehoogeveen-medweb
2022-03-03 06:33:32 -08:00
pukkandan
747c0bd127
[utils] Improve file locking
* Implement non-blocking locks for windows
* Don't raise error when closing a closed file
2022-03-03 19:28:47 +05:30
Justin Keogh
acea8d7cfb
[utils] Fix file locking for AOSP (#2714)
Closes #2080, #2670

Authored by: jakeogh
2022-03-03 05:09:00 -08:00
5 changed files with 64 additions and 37 deletions

View File

@ -210,28 +210,41 @@ class FileDownloader(object):
def ytdl_filename(self, filename):
return filename + '.ytdl'
def sanitize_open(self, filename, open_mode):
file_access_retries = self.params.get('file_access_retries', 10)
retry = 0
while True:
try:
return sanitize_open(filename, open_mode)
except (IOError, OSError) as err:
retry = retry + 1
if retry > file_access_retries or err.errno not in (errno.EACCES,):
raise
self.to_screen(
'[download] Got file access error. Retrying (attempt %d of %s) ...'
% (retry, self.format_retries(file_access_retries)))
time.sleep(0.01)
def wrap_file_access(action, *, fatal=False):
def outer(func):
def inner(self, *args, **kwargs):
file_access_retries = self.params.get('file_access_retries', 0)
retry = 0
while True:
try:
return func(self, *args, **kwargs)
except (IOError, OSError) as err:
retry = retry + 1
if retry > file_access_retries or err.errno not in (errno.EACCES, errno.EINVAL):
if not fatal:
self.report_error(f'unable to {action} file: {err}')
return
raise
self.to_screen(
f'[download] Unable to {action} file due to file access error. '
f'Retrying (attempt {retry} of {self.format_retries(file_access_retries)}) ...')
time.sleep(0.01)
return inner
return outer
@wrap_file_access('open', fatal=True)
def sanitize_open(self, filename, open_mode):
return sanitize_open(filename, open_mode)
@wrap_file_access('remove')
def try_remove(self, filename):
os.remove(filename)
@wrap_file_access('rename')
def try_rename(self, old_filename, new_filename):
if old_filename == new_filename:
return
try:
os.replace(old_filename, new_filename)
except (IOError, OSError) as err:
self.report_error(f'unable to rename file: {err}')
os.replace(old_filename, new_filename)
def try_utime(self, filename, last_modified_hdr):
"""Try to set the last-modified time of the given file."""

View File

@ -159,9 +159,9 @@ class ExternalFD(FragmentFD):
dest.write(decrypt_fragment(fragment, src.read()))
src.close()
if not self.params.get('keep_fragments', False):
os.remove(encodeFilename(fragment_filename))
self.try_remove(encodeFilename(fragment_filename))
dest.close()
os.remove(encodeFilename('%s.frag.urls' % tmpfilename))
self.try_remove(encodeFilename('%s.frag.urls' % tmpfilename))
return 0

View File

@ -159,7 +159,7 @@ class FragmentFD(FileDownloader):
if self.__do_ytdl_file(ctx):
self._write_ytdl_file(ctx)
if not self.params.get('keep_fragments', False):
os.remove(encodeFilename(ctx['fragment_filename_sanitized']))
self.try_remove(encodeFilename(ctx['fragment_filename_sanitized']))
del ctx['fragment_filename_sanitized']
def _prepare_frag_download(self, ctx):
@ -305,7 +305,7 @@ class FragmentFD(FileDownloader):
if self.__do_ytdl_file(ctx):
ytdl_filename = encodeFilename(self.ytdl_filename(ctx['filename']))
if os.path.isfile(ytdl_filename):
os.remove(ytdl_filename)
self.try_remove(ytdl_filename)
elapsed = time.time() - ctx['started']
if ctx['tmpfilename'] == '-':

View File

@ -727,7 +727,7 @@ def create_parser():
help='Number of retries (default is %default), or "infinite"')
downloader.add_option(
'--file-access-retries',
dest='file_access_retries', metavar='RETRIES', default=10,
dest='file_access_retries', metavar='RETRIES', default=3,
help='Number of times to retry on file access error (default is %default), or "infinite"')
downloader.add_option(
'--fragment-retries',

View File

@ -2122,37 +2122,47 @@ if sys.platform == 'win32':
whole_low = 0xffffffff
whole_high = 0x7fffffff
def _lock_file(f, exclusive, block): # todo: block unused on win32
def _lock_file(f, exclusive, block):
overlapped = OVERLAPPED()
overlapped.Offset = 0
overlapped.OffsetHigh = 0
overlapped.hEvent = 0
f._lock_file_overlapped_p = ctypes.pointer(overlapped)
handle = msvcrt.get_osfhandle(f.fileno())
if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
whole_low, whole_high, f._lock_file_overlapped_p):
raise OSError('Locking file failed: %r' % ctypes.FormatError())
if not LockFileEx(msvcrt.get_osfhandle(f.fileno()),
(0x2 if exclusive else 0x0) | (0x0 if block else 0x1),
0, whole_low, whole_high, f._lock_file_overlapped_p):
raise BlockingIOError('Locking file failed: %r' % ctypes.FormatError())
def _unlock_file(f):
assert f._lock_file_overlapped_p
handle = msvcrt.get_osfhandle(f.fileno())
if not UnlockFileEx(handle, 0,
whole_low, whole_high, f._lock_file_overlapped_p):
if not UnlockFileEx(handle, 0, whole_low, whole_high, f._lock_file_overlapped_p):
raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
else:
# Some platforms, such as Jython, is missing fcntl
try:
import fcntl
def _lock_file(f, exclusive, block):
fcntl.flock(f,
fcntl.LOCK_SH if not exclusive
else fcntl.LOCK_EX if block
else fcntl.LOCK_EX | fcntl.LOCK_NB)
try:
fcntl.flock(f,
fcntl.LOCK_SH if not exclusive
else fcntl.LOCK_EX if block
else fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise
except OSError: # AOSP does not have flock()
fcntl.lockf(f,
fcntl.LOCK_SH if not exclusive
else fcntl.LOCK_EX if block
else fcntl.LOCK_EX | fcntl.LOCK_NB)
def _unlock_file(f):
fcntl.flock(f, fcntl.LOCK_UN)
try:
fcntl.flock(f, fcntl.LOCK_UN)
except OSError:
fcntl.lockf(f, fcntl.LOCK_UN)
except ImportError:
UNSUPPORTED_MSG = 'file locking is not supported on this platform'
@ -2165,6 +2175,8 @@ else:
class locked_file(object):
_closed = False
def __init__(self, filename, mode, block=True, encoding=None):
assert mode in ['r', 'rb', 'a', 'ab', 'w', 'wb']
self.f = io.open(filename, mode, encoding=encoding)
@ -2182,9 +2194,11 @@ class locked_file(object):
def __exit__(self, etype, value, traceback):
try:
_unlock_file(self.f)
if not self._closed:
_unlock_file(self.f)
finally:
self.f.close()
self._closed = True
def __iter__(self):
return iter(self.f)