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:
parent
a367a0df1d
commit
d8e48c0511
2 changed files with 127 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue