loop: add clear_fd_wait method

Add a helper method that clears the fd for a given loop device but
also ensures that the loop device is not bound to the supplied fd
anymore. Check the function documentation for more information.
Add a corresponding test.
This commit is contained in:
Christian Kellner 2021-08-08 13:32:25 +00:00 committed by Tom Gundersen
parent a367a0df1d
commit d8e48c0511
2 changed files with 127 additions and 0 deletions

View file

@ -4,6 +4,7 @@ import errno
import fcntl
import os
import stat
import time
__all__ = [
@ -160,6 +161,61 @@ class Loop:
fcntl.ioctl(self.fd, self.LOOP_CLR_FD)
def clear_fd_wait(self, fd: int, timeout: float, wait: float = 0.1) -> None:
"""Wait until the file descriptor is cleared
When clearing the file descriptor of the loopback device the
kernel will check if the loop device has a reference count
greater then one(!), i.e. if another fd besied the one trying
to clear the loopback device is open. If so it will only set
the `LO_FLAGS_AUTOCLEAR` flag and wait until the the device
is released. This means we cannot be sure the loopback device
is actually cleared.
To alleviated this situation we wait until the the loop is not
bound anymore or not bound to `fd` anymore (in case someone
else bound it between checks).
Raises a `TimeoutError` if the file descriptor when `timeout`
is reached.
Parameters
----------
fd : int
the file descriptor to wait for
timeout : float
the maximum time to wait in seconds
wait : float
the time to wait between each check in seconds
"""
file_info = os.fstat(fd)
endtime = time.monotonic() + timeout
# wait until the loop device is unbound, which means calling
# `get_status` will fail with `ENXIO` or if someone raced us
# and bound the loop device again, it is not backed by "our"
# file descriptor specified via `fd` anymore
while True:
try:
self.clear_fd()
loop_info = self.get_status()
except OSError as err:
# check if the loop is still bound
if err.errno == errno.ENXIO:
return
# check if it is backed by the fd
if not loop_info.is_bound_to(file_info):
return
if time.monotonic() > endtime:
raise TimeoutError("waiting for loop device timed out")
time.sleep(wait)
def change_fd(self, fd):
"""Replace the bound filedescriptor

View file

@ -4,6 +4,8 @@
import contextlib
import os
import time
import threading
from tempfile import TemporaryDirectory, TemporaryFile
import pytest
@ -87,3 +89,72 @@ def test_basic(tempdir):
with pytest.raises(RuntimeError):
ctl.loop_for_fd(0)
@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_clear_fd_wait(tempdir):
path = os.path.join(tempdir, "test.img")
ctl = loop.LoopControl()
assert ctl
delay_time = 0.25
def close_loop(lo, barrier):
barrier.wait()
time.sleep(delay_time)
print("closing loop")
lo.close()
lo, lo2, f = None, None, None
try:
f = open(path, "wb+")
f.truncate(1024)
f.flush()
lo = ctl.loop_for_fd(f.fileno(), autoclear=False)
assert lo
# Increase reference count of the loop to > 1 thus
# preventing the kernel from immediately closing the
# device. Instead the kernel will set the autoclear
# attribute and return
lo2 = loop.Loop(lo.minor)
assert lo2
# as long as the second loop is alive, the kernel can
# not clear the fd and thus we will get a timeout
with pytest.raises(TimeoutError):
lo.clear_fd_wait(f.fileno(), 0.1, 0.01)
# start a thread and sync with a barrier, then close
# the loop device in the background thread while the
# main thread is waiting in `clear_fd_wait`. We wait
# four times the delay time of the thread to ensure
# we don't get a timeout.
barrier = threading.Barrier(2)
thread = threading.Thread(
target=close_loop,
args=(lo2, barrier)
)
barrier.reset()
thread.start()
barrier.wait()
lo.clear_fd_wait(f.fileno(), 4*delay_time, delay_time/10)
# no timeout exception has occurred and thus the device
# must not be be bound to the original file anymore
assert not lo.is_bound_to(f.fileno())
finally:
if lo2:
lo2.close()
if lo:
with contextlib.suppress(OSError):
lo.clear_fd()
lo.close()
if f:
f.close()
ctl.close()