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.
124 lines
4.5 KiB
Python
124 lines
4.5 KiB
Python
"""Callback class definition for BAT Pack progress reporting."""
|
|
import threading
|
|
|
|
import functools
|
|
import logging
|
|
import pathlib
|
|
import queue
|
|
import typing
|
|
|
|
import blender_asset_tracer.trace.progress
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class Callback(blender_asset_tracer.trace.progress.Callback):
|
|
"""BAT Pack progress reporting."""
|
|
|
|
def pack_start(self) -> None:
|
|
"""Called when packing starts."""
|
|
|
|
def pack_done(self,
|
|
output_blendfile: pathlib.Path,
|
|
missing_files: typing.Set[pathlib.Path]) -> None:
|
|
"""Called when packing is done."""
|
|
|
|
def trace_blendfile(self, filename: pathlib.Path) -> None:
|
|
"""Called for every blendfile opened when tracing dependencies."""
|
|
|
|
def trace_asset(self, filename: pathlib.Path) -> None:
|
|
"""Called for every asset found when tracing dependencies.
|
|
|
|
Note that this can also be a blend file.
|
|
"""
|
|
|
|
def rewrite_blendfile(self, orig_filename: pathlib.Path) -> None:
|
|
"""Called for every rewritten blendfile."""
|
|
|
|
def transfer_file(self, src: pathlib.Path, dst: pathlib.Path) -> None:
|
|
"""Called when a file transfer starts."""
|
|
|
|
def transfer_file_skipped(self, src: pathlib.Path, dst: pathlib.Path) -> None:
|
|
"""Called when a file is skipped because it already exists."""
|
|
|
|
def transfer_progress(self, total_bytes: int, transferred_bytes: int) -> None:
|
|
"""Called during file transfer, with per-pack info (not per file).
|
|
|
|
:param total_bytes: The total amount of bytes to be transferred for
|
|
the current packing operation. This can increase while transfer
|
|
is happening, when more files are discovered (because transfer
|
|
starts in a separate thread before all files are found).
|
|
:param transferred_bytes: The total amount of bytes transfered for
|
|
the current packing operation.
|
|
"""
|
|
|
|
def missing_file(self, filename: pathlib.Path) -> None:
|
|
"""Called for every asset that does not exist on the filesystem."""
|
|
|
|
|
|
class ThreadSafeCallback(Callback):
|
|
"""Thread-safe wrapper for Callback instances.
|
|
|
|
Progress calls are queued until flush() is called. The queued calls are
|
|
called in the same thread as the one calling flush().
|
|
"""
|
|
|
|
def __init__(self, wrapped: Callback) -> None:
|
|
self.log = log.getChild('ThreadSafeCallback')
|
|
self.wrapped = wrapped
|
|
|
|
# Thread-safe queue for passing progress reports on the main thread.
|
|
self._reporting_queue = queue.Queue() # type: queue.Queue[typing.Callable]
|
|
self._main_thread_id = threading.get_ident()
|
|
|
|
def _queue(self, func: typing.Callable, *args, **kwargs):
|
|
partial = functools.partial(func, *args, **kwargs)
|
|
|
|
if self._main_thread_id == threading.get_ident():
|
|
partial()
|
|
else:
|
|
self._reporting_queue.put(partial)
|
|
|
|
def pack_start(self) -> None:
|
|
self._queue(self.wrapped.pack_start)
|
|
|
|
def pack_done(self,
|
|
output_blendfile: pathlib.Path,
|
|
missing_files: typing.Set[pathlib.Path]) -> None:
|
|
self._queue(self.wrapped.pack_done, output_blendfile, missing_files)
|
|
|
|
def trace_blendfile(self, filename: pathlib.Path) -> None:
|
|
self._queue(self.wrapped.trace_blendfile, filename)
|
|
|
|
def trace_asset(self, filename: pathlib.Path) -> None:
|
|
self._queue(self.wrapped.trace_asset, filename)
|
|
|
|
def transfer_file(self, src: pathlib.Path, dst: pathlib.Path) -> None:
|
|
self._queue(self.wrapped.transfer_file, src, dst)
|
|
|
|
def transfer_file_skipped(self, src: pathlib.Path, dst: pathlib.Path) -> None:
|
|
self._queue(self.wrapped.transfer_file_skipped, src, dst)
|
|
|
|
def transfer_progress(self, total_bytes: int, transferred_bytes: int) -> None:
|
|
self._queue(self.wrapped.transfer_progress, total_bytes, transferred_bytes)
|
|
|
|
def missing_file(self, filename: pathlib.Path) -> None:
|
|
self._queue(self.wrapped.missing_file, filename)
|
|
|
|
def flush(self, timeout: float = None) -> None:
|
|
"""Call the queued calls, call this in the main thread."""
|
|
|
|
while not self._reporting_queue.empty():
|
|
try:
|
|
call = self._reporting_queue.get(block=timeout is not None,
|
|
timeout=timeout)
|
|
except queue.Empty:
|
|
return
|
|
|
|
try:
|
|
call()
|
|
except Exception:
|
|
# Don't let the handling of one callback call
|
|
# block the entire flush process.
|
|
self.log.exception('Error calling %s', call)
|