Implemented reporting callbacks.

Both the dependency Tracer class and the Packer class now support a
callback object, where the latter is a subclass of the former.

For file transfers running in a separate thread, there is a thread-safe
wrapper for progress callbacks. This wrapper can be called from any thread,
and calls the wrapped callback object from the main thread. This way the
callback implementation itself doesn't have to worry about threading
issues.
This commit is contained in:
Sybren A. Stüvel
2018-03-15 17:59:55 +01:00
parent d2e13750e9
commit 889f3abd25
10 changed files with 421 additions and 68 deletions
+48 -14
View File
@@ -4,8 +4,11 @@ import logging
import pathlib
import queue
import threading
import time
import typing
from . import progress
log = logging.getLogger(__name__)
@@ -25,11 +28,16 @@ class Action(enum.Enum):
QueueItem = typing.Tuple[pathlib.Path, pathlib.Path, Action]
class FileTransferer(metaclass=abc.ABCMeta):
"""Interface for file transfer classes."""
class FileTransferer(threading.Thread, metaclass=abc.ABCMeta):
"""Abstract superclass for file transfer classes.
Implement a run() function in a subclass that performs the actual file
transfer.
"""
def __init__(self) -> None:
super().__init__()
self.log = log.getChild('FileTransferer')
# For copying in a different process. By using a priority queue the files
# are automatically sorted alphabetically, which means we go through all files
@@ -44,17 +52,35 @@ class FileTransferer(metaclass=abc.ABCMeta):
self.done = threading.Event()
self.abort = threading.Event()
# Instantiate a dummy progress callback so that we can call it
# without checking for None all the time.
self.progress_cb = progress.ThreadSafeCallback(progress.Callback())
self.total_queued_bytes = 0
self.total_transferred_bytes = 0
@abc.abstractmethod
def run(self):
"""Perform actual file transfer in a thread."""
def queue_copy(self, src: pathlib.Path, dst: pathlib.Path):
"""Queue a copy action from 'src' to 'dst'."""
assert not self.done.is_set(), 'Queueing not allowed after done_and_join() was called'
assert not self.abort.is_set(), 'Queueing not allowed after abort_and_join() was called'
self.queue.put((src, dst, Action.COPY))
self.total_queued_bytes += src.stat().st_size
def queue_move(self, src: pathlib.Path, dst: pathlib.Path):
"""Queue a move action from 'src' to 'dst'."""
assert not self.done.is_set(), 'Queueing not allowed after done_and_join() was called'
assert not self.abort.is_set(), 'Queueing not allowed after abort_and_join() was called'
self.queue.put((src, dst, Action.MOVE))
self.total_queued_bytes += src.stat().st_size
def report_transferred(self, block_size: int):
"""Report transfer of `block_size` bytes."""
self.total_transferred_bytes += block_size
self.progress_cb.transfer_progress(self.total_queued_bytes, self.total_transferred_bytes)
def done_and_join(self) -> None:
"""Indicate all files have been queued, and wait until done.
@@ -105,21 +131,29 @@ class FileTransferer(metaclass=abc.ABCMeta):
return
try:
yield self.queue.get(timeout=0.1)
src, dst, action = self.queue.get(timeout=0.1)
self.progress_cb.transfer_file(src, dst)
yield src, dst, action
except queue.Empty:
if self.done.is_set():
return
@abc.abstractmethod
def start(self) -> None:
"""Starts the file transfer thread/process.
def join(self, timeout: float = None) -> None:
"""Wait for the transfer to finish/stop."""
This could spin up a separate thread to perform the actual file
transfer. After start() is called, implementations should still accept
calls to the queue_xxx() methods. In other words, this is not to be
used as queue-and-then-start, but as start-and-then-queue.
"""
if timeout:
run_until = time.time() + timeout
else:
run_until = float('inf')
@abc.abstractmethod
def join(self, timeout=None):
"""Wait for the thread/process to stop."""
# We can't simply block the thread, we have to keep watching the
# progress queue.
while self.is_alive():
if time.time() > run_until:
self.log.warning('Timeout while waiting for transfer to finish')
return
self.progress_cb.flush(timeout=0.1)
# Since Thread.join() neither returns anything nor raises any exception
# when timing out, we don't even have to call it any more.