work: restore shift+spacebar for media play/pause
maybe put in maya config? idk what funiman's preference is
This commit is contained in:
@@ -3,29 +3,33 @@
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from urllib3.exceptions import HTTPError, MaxRetryError
|
||||
from pathlib import Path, PurePath, PurePosixPath
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import bpy
|
||||
from urllib3.exceptions import HTTPError, MaxRetryError
|
||||
|
||||
from . import job_types, job_submission, preferences, manager_info
|
||||
from .job_types_propgroup import JobTypePropertyGroup
|
||||
from . import job_submission, job_types, manager_info, preferences
|
||||
from .bat.submodules import bpathlib
|
||||
from .job_types_propgroup import JobTypePropertyGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .bat.interface import (
|
||||
PackThread as _PackThread,
|
||||
Message as _Message,
|
||||
)
|
||||
from .manager.models import (
|
||||
Error as _Error,
|
||||
SubmittedJob as _SubmittedJob,
|
||||
from .bat.interface import (
|
||||
PackThread as _PackThread,
|
||||
)
|
||||
from .manager.api_client import ApiClient as _ApiClient
|
||||
from .manager.exceptions import ApiException as _ApiException
|
||||
from .manager.models import (
|
||||
Error as _Error,
|
||||
)
|
||||
from .manager.models import (
|
||||
SubmittedJob as _SubmittedJob,
|
||||
)
|
||||
else:
|
||||
_PackThread = object
|
||||
_Message = object
|
||||
@@ -34,6 +38,21 @@ else:
|
||||
_ApiException = object
|
||||
_Error = object
|
||||
|
||||
# Conditionally import BAT v2, as that version requires Blender 5.1+.
|
||||
#
|
||||
# Blender 5.1.0 still had some limitations, most importantly missing support for geometry nodes
|
||||
# simulation caches (https://projects.blender.org/blender/blender/issues/155953). That should be
|
||||
# fixed in 5.1.1 though.
|
||||
bat_v2: ModuleType | None
|
||||
if bpy.app.version >= (5, 1, 1) or TYPE_CHECKING:
|
||||
from . import bat_v2
|
||||
from .bat_v2.pack_fs import BATPacker
|
||||
|
||||
_BATPacker = BATPacker
|
||||
else:
|
||||
bat_v2 = None
|
||||
_BATPacker = object
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -111,15 +130,21 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
|
||||
job_name: bpy.props.StringProperty(name="Job Name") # type: ignore
|
||||
job: Optional[_SubmittedJob] = None
|
||||
temp_blendfile: Optional[Path] = None
|
||||
ignore_version_mismatch: bpy.props.BoolProperty( # type: ignore
|
||||
name="Ignore Version Mismatch",
|
||||
default=False,
|
||||
)
|
||||
|
||||
TIMER_PERIOD = 0.25 # seconds
|
||||
TIMER_PERIOD_BAT_V2 = 0.01 # seconds
|
||||
|
||||
timer: Optional[bpy.types.Timer] = None
|
||||
# For BAT v1:
|
||||
packthread: Optional[_PackThread] = None
|
||||
# For BAT v2:
|
||||
bat_v2_packer: _BATPacker | None = None
|
||||
bat_v2_packer_reported_error: bool = False
|
||||
bat_v2_packer_missing_files: list[Path]
|
||||
|
||||
log = _log.getChild(bl_idname)
|
||||
|
||||
@@ -131,55 +156,96 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
return job_type is not None
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> set[str]:
|
||||
"""Submit the job files in a blocking way.
|
||||
|
||||
This allows scripted submission, which blocks the main thread until
|
||||
the process is done.
|
||||
"""
|
||||
|
||||
filepath, ok = self._presubmit_check(context)
|
||||
if not ok:
|
||||
return {"CANCELLED"}
|
||||
|
||||
is_running = self._submit_files(context, filepath)
|
||||
# Use BAT v2 if available.
|
||||
if bat_v2 is not None:
|
||||
is_running = self._submit_files_bat_v2(context, filepath)
|
||||
if not is_running:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# self.bat_v2_packer is None when no packing is necessary, that is, when the file is
|
||||
# already on the shared storage.
|
||||
while self.bat_v2_packer and self.bat_v2_packer.step():
|
||||
pass
|
||||
|
||||
return self.bat_v2_packer_finalize_and_quit(context)
|
||||
|
||||
is_running = self._submit_files_bat_v1(context, filepath)
|
||||
if not is_running:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.packthread is None:
|
||||
# If there is no pack thread running, there isn't much we can do.
|
||||
self.report({"ERROR"}, "No pack thread running, please report a bug")
|
||||
self._quit(context)
|
||||
return {"CANCELLED"}
|
||||
# Keep handling messages from the background thread. That's only necessary if there is a
|
||||
# background thread.
|
||||
if self.packthread:
|
||||
while True:
|
||||
# Block for 5 seconds at a time. The exact duration doesn't matter,
|
||||
# as this while-loop is blocking the main thread anyway.
|
||||
msg = self.packthread.poll(timeout=5)
|
||||
if not msg:
|
||||
# No message received, is fine, just wait for another one.
|
||||
continue
|
||||
|
||||
# Keep handling messages from the background thread.
|
||||
while True:
|
||||
# Block for 5 seconds at a time. The exact duration doesn't matter,
|
||||
# as this while-loop is blocking the main thread anyway.
|
||||
msg = self.packthread.poll(timeout=5)
|
||||
if not msg:
|
||||
# No message received, is fine, just wait for another one.
|
||||
continue
|
||||
|
||||
result = self._on_bat_pack_msg(context, msg)
|
||||
if "RUNNING_MODAL" not in result:
|
||||
break
|
||||
result = self._on_bat_pack_msg(context, msg)
|
||||
if "RUNNING_MODAL" not in result:
|
||||
break
|
||||
self.packthread.join(timeout=5)
|
||||
|
||||
self._quit(context)
|
||||
self.packthread.join(timeout=5)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
|
||||
|
||||
filepath, ok = self._presubmit_check(context)
|
||||
if not ok:
|
||||
return {"CANCELLED"}
|
||||
|
||||
is_running = self._submit_files(context, filepath)
|
||||
if bat_v2 is None:
|
||||
is_running = self._submit_files_bat_v1(context, filepath)
|
||||
else:
|
||||
is_running = self._submit_files_bat_v2(context, filepath)
|
||||
if not is_running:
|
||||
return {"CANCELLED"}
|
||||
|
||||
context.window_manager.modal_handler_add(self)
|
||||
if bat_v2:
|
||||
# Only BATv2 supports aborting the packing.
|
||||
self.report({"INFO"}, "Flamenco: Submitting files, press ESC to abort")
|
||||
|
||||
wm = context.window_manager
|
||||
self.timer = wm.event_timer_add(self.TIMER_PERIOD_BAT_V2, window=context.window)
|
||||
wm.modal_handler_add(self)
|
||||
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def modal(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
|
||||
wm = bpy.context.window_manager
|
||||
|
||||
# Only BAT v2 has support for aborting the packing operation.
|
||||
should_abort = event.type == "ESC" or wm.flamenco_bat_status == "ABORTING"
|
||||
if self.bat_v2_packer is not None and should_abort:
|
||||
self.report({"WARNING"}, "Flamenco: Job submission aborted")
|
||||
wm.flamenco_bat_status = "ABORTED"
|
||||
wm.flamenco_bat_status_txt = ""
|
||||
return self._quit(context)
|
||||
|
||||
# This function is called for TIMER events to poll the BAT pack thread.
|
||||
if event.type != "TIMER":
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
if self.bat_v2_packer is not None:
|
||||
# BAT v2 pack is underway.
|
||||
keep_going = self.bat_v2_packer.step()
|
||||
if keep_going:
|
||||
return {"RUNNING_MODAL"}
|
||||
return self.bat_v2_packer_finalize_and_quit(context)
|
||||
|
||||
if self.packthread is None:
|
||||
# If there is no pack thread running, there isn't much we can do.
|
||||
return self._quit(context)
|
||||
@@ -304,13 +370,7 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
We shouldn't overwrite the artist's file.
|
||||
We can compress, since this file won't be managed by SVN and doesn't need diffability.
|
||||
"""
|
||||
render = context.scene.render
|
||||
prefs = context.preferences
|
||||
|
||||
# Remember settings we need to restore after saving.
|
||||
old_use_file_extension = render.use_file_extension
|
||||
old_use_overwrite = render.use_overwrite
|
||||
old_use_placeholder = render.use_placeholder
|
||||
old_use_all_linked_data_direct = getattr(
|
||||
prefs.experimental, "use_all_linked_data_direct", None
|
||||
)
|
||||
@@ -318,44 +378,23 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
# TODO: see about disabling the denoiser (like the old Blender Cloud addon did).
|
||||
|
||||
try:
|
||||
# The file extension should be determined by the render settings, not necessarily
|
||||
# by the settings in the output panel.
|
||||
render.use_file_extension = True
|
||||
|
||||
# Rescheduling should not overwrite existing frames.
|
||||
render.use_overwrite = False
|
||||
render.use_placeholder = False
|
||||
|
||||
# To work around a shortcoming of BAT, ensure that all
|
||||
# To work around a shortcoming of BAT v1, ensure that all
|
||||
# indirectly-linked data is still saved as directly-linked.
|
||||
#
|
||||
# See `133dde41bb5b: Improve handling of (in)directly linked status
|
||||
# for linked IDs` in Blender's Git repository.
|
||||
if old_use_all_linked_data_direct is not None:
|
||||
if bat_v2 is None and old_use_all_linked_data_direct is not None:
|
||||
self.log.info(
|
||||
"Overriding prefs.experimental.use_all_linked_data_direct = True"
|
||||
)
|
||||
prefs.experimental.use_all_linked_data_direct = True
|
||||
|
||||
filepath = Path(context.blend_data.filepath)
|
||||
if job_submission.is_file_inside_job_storage(context, filepath):
|
||||
self.log.info(
|
||||
"Saving blendfile, already in shared storage: %s", filepath
|
||||
)
|
||||
if bpy.data.is_dirty:
|
||||
self.log.info("Saving blendfile: %s", filepath)
|
||||
bpy.ops.wm.save_as_mainfile()
|
||||
else:
|
||||
filepath = filepath.with_suffix(".flamenco.blend")
|
||||
self.log.info("Saving copy to temporary file %s", filepath)
|
||||
bpy.ops.wm.save_as_mainfile(
|
||||
filepath=str(filepath), compress=True, copy=True
|
||||
)
|
||||
self.temp_blendfile = filepath
|
||||
finally:
|
||||
# Restore the settings we changed, even after an exception.
|
||||
render.use_file_extension = old_use_file_extension
|
||||
render.use_overwrite = old_use_overwrite
|
||||
render.use_placeholder = old_use_placeholder
|
||||
|
||||
# Only restore if the property exists to begin with:
|
||||
if old_use_all_linked_data_direct is not None:
|
||||
prefs.experimental.use_all_linked_data_direct = (
|
||||
@@ -364,135 +403,183 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
|
||||
return filepath
|
||||
|
||||
def _bat_project_path(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> Path:
|
||||
"""BAT 'project' directory: preference root, or wider if links lie outside it.
|
||||
|
||||
Linked blends outside the configured project force BAT to use KEEP_PATH;
|
||||
Shaman packing can then fail path rewriting. Using a common ancestor of
|
||||
the submission blend and all linked library paths keeps assets inside
|
||||
the project tree for BAT.
|
||||
"""
|
||||
prefs = preferences.get(context)
|
||||
configured = bpathlib.make_absolute(
|
||||
Path(bpy.path.abspath(str(prefs.project_root())))
|
||||
)
|
||||
blend_abs = bpathlib.make_absolute(blendfile)
|
||||
|
||||
lib_paths: list[Path] = []
|
||||
for lib in bpy.data.libraries:
|
||||
if not lib.filepath:
|
||||
continue
|
||||
lib_paths.append(
|
||||
bpathlib.make_absolute(Path(bpy.path.abspath(lib.filepath)))
|
||||
)
|
||||
|
||||
def is_under(root: Path, path: Path) -> bool:
|
||||
try:
|
||||
path.resolve().relative_to(root.resolve())
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
need_widen = (not is_under(configured, blend_abs)) or any(
|
||||
not is_under(configured, lp) for lp in lib_paths
|
||||
)
|
||||
if not need_widen:
|
||||
return configured
|
||||
|
||||
all_paths = [blend_abs] + lib_paths
|
||||
try:
|
||||
common = Path(os.path.commonpath([str(p) for p in all_paths])).resolve()
|
||||
except ValueError:
|
||||
self.log.warning(
|
||||
"Could not compute common path for BAT project root, using preferences"
|
||||
)
|
||||
return configured
|
||||
|
||||
self.log.info(
|
||||
"BAT project root widened from %s to %s (assets outside preference project)",
|
||||
configured,
|
||||
common,
|
||||
)
|
||||
return common
|
||||
|
||||
def _convert_relpaths_to_absolute(self, context: bpy.types.Context) -> None:
|
||||
"""Convert all relative paths in the blend file to absolute paths.
|
||||
|
||||
Covers libraries, images, clips, sounds, fonts, volumes, and cache_files
|
||||
(point caches / GN caches). Allows the blend to be sent as-is without BAT.
|
||||
"""
|
||||
# Convert library paths to absolute
|
||||
for library in bpy.data.libraries:
|
||||
if library.filepath:
|
||||
old_path = library.filepath
|
||||
abs_path = bpy.path.abspath(library.filepath)
|
||||
library.filepath = abs_path
|
||||
self.log.debug("Converted library path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert image paths to absolute
|
||||
for image in bpy.data.images:
|
||||
if image.filepath and not image.packed_file:
|
||||
old_path = image.filepath
|
||||
abs_path = bpy.path.abspath(image.filepath)
|
||||
image.filepath = abs_path
|
||||
self.log.debug("Converted image path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert movie paths to absolute
|
||||
for movie in bpy.data.movieclips:
|
||||
if movie.filepath:
|
||||
old_path = movie.filepath
|
||||
abs_path = bpy.path.abspath(movie.filepath)
|
||||
movie.filepath = abs_path
|
||||
self.log.debug("Converted movie path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert sound paths to absolute
|
||||
for sound in bpy.data.sounds:
|
||||
if sound.filepath:
|
||||
old_path = sound.filepath
|
||||
abs_path = bpy.path.abspath(sound.filepath)
|
||||
sound.filepath = abs_path
|
||||
self.log.debug("Converted sound path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert font paths to absolute (skip VectorFont - its filepath is read-only)
|
||||
for font in bpy.data.fonts:
|
||||
if font.filepath:
|
||||
try:
|
||||
old_path = font.filepath
|
||||
abs_path = bpy.path.abspath(font.filepath)
|
||||
font.filepath = abs_path
|
||||
self.log.debug("Converted font path: %s -> %s", old_path, abs_path)
|
||||
except (TypeError, AttributeError):
|
||||
self.log.debug("Skipping font %s (filepath is read-only)", font.name)
|
||||
|
||||
# Convert volume paths to absolute
|
||||
for volume in bpy.data.volumes:
|
||||
if volume.filepath:
|
||||
old_path = volume.filepath
|
||||
abs_path = bpy.path.abspath(volume.filepath)
|
||||
volume.filepath = abs_path
|
||||
self.log.debug("Converted volume path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Point / mesh cache files (e.g. .pc2), geometry-nodes caches, etc.
|
||||
for cache_file in bpy.data.cache_files:
|
||||
if cache_file.filepath:
|
||||
old_path = cache_file.filepath
|
||||
abs_path = bpy.path.abspath(cache_file.filepath)
|
||||
cache_file.filepath = abs_path
|
||||
self.log.debug("Converted cache_file path: %s -> %s", old_path, abs_path)
|
||||
|
||||
def _submit_files(self, context: bpy.types.Context, blendfile: Path) -> bool:
|
||||
def _submit_files_bat_v2(self, context: bpy.types.Context, blendfile: Path) -> bool:
|
||||
"""Ensure that the files are somewhere in the shared storage.
|
||||
|
||||
Returns True if a packing thread has been started, and False otherwise.
|
||||
"""
|
||||
prefs = preferences.get(context)
|
||||
if prefs.bat_bypass:
|
||||
return self._submit_files_bat_bypass(context, blendfile)
|
||||
|
||||
from .bat_v2 import bat_version, pack_fs, pack_shaman
|
||||
|
||||
self.log.info("Submitting files via BAT %s", bat_version())
|
||||
|
||||
# Reset state from any previous run.
|
||||
self.bat_v2_packer = None
|
||||
self.bat_v2_packer_reported_error = False
|
||||
self.bat_v2_packer_missing_files = []
|
||||
|
||||
manager = self._manager_info(context)
|
||||
if not manager:
|
||||
return False
|
||||
|
||||
# Get the project root and double-check its existence.
|
||||
prefs = preferences.get(context)
|
||||
project_path: Path = prefs.project_root()
|
||||
assert project_path.is_absolute(), (
|
||||
"Expecting project path {!s} to be an absolute path".format(project_path)
|
||||
)
|
||||
if not project_path.exists():
|
||||
self.report(
|
||||
{"ERROR"}, "Project path {!s} does not exist".format(project_path)
|
||||
)
|
||||
raise FileNotFoundError()
|
||||
|
||||
if job_submission.is_file_inside_job_storage(context, blendfile):
|
||||
self.log.info(
|
||||
"File is already in job storage location, submitting it as-is"
|
||||
)
|
||||
self._use_blendfile_directly(context, blendfile)
|
||||
return True
|
||||
|
||||
if manager.shared_storage.shaman_enabled:
|
||||
# Pack to the Shaman server.
|
||||
self.log.info("Copying BAT pack to Shaman storage")
|
||||
batpacker = pack_shaman.pack_start(
|
||||
project_root=project_path,
|
||||
reporter=self,
|
||||
use_relative_only=prefs.use_relative_only,
|
||||
api_client=self.get_api_client(context),
|
||||
checkout_path=PurePosixPath(self.job_name),
|
||||
ignore_globs=prefs.ignore_globs(),
|
||||
)
|
||||
batpacker.start()
|
||||
|
||||
# When packing via Shaman, the Shaman server determines the final
|
||||
# location of the blend file, and so it's not known yet.
|
||||
self.blendfile_on_farm = None
|
||||
else:
|
||||
# Pack to the filesystem.
|
||||
unique_dir = "%s-%s" % (
|
||||
datetime.datetime.now().isoformat("-").replace(":", ""),
|
||||
self.job_name,
|
||||
)
|
||||
pack_target_dir = Path(manager.shared_storage.location) / unique_dir
|
||||
self.log.info("Copying BAT pack to shared storage: %s", pack_target_dir)
|
||||
batpacker = pack_fs.pack_start(
|
||||
project_root=project_path,
|
||||
reporter=self,
|
||||
use_relative_only=prefs.use_relative_only,
|
||||
pack_target_dir=pack_target_dir,
|
||||
ignore_globs=prefs.ignore_globs(),
|
||||
)
|
||||
batpacker.start()
|
||||
|
||||
# When packing to the filesystem, the final path of the file on the
|
||||
# farm is known immediately.
|
||||
source_file_info = batpacker.source_file_info()
|
||||
abspath_on_farm = pack_target_dir / source_file_info.relpath_in_pack
|
||||
self.blendfile_on_farm = PurePosixPath(abspath_on_farm.as_posix())
|
||||
self.log.info(" %s", abspath_on_farm)
|
||||
|
||||
self.bat_v2_packer = batpacker
|
||||
|
||||
# Start the timer for periodic updates of the packing process. This
|
||||
# needs a relatively fast update cycle, as each file to be copied needs
|
||||
# its own update.
|
||||
#
|
||||
# TODO: if blocking the UI for each file copy gets too annoying, move
|
||||
# the process to a separate thread.
|
||||
wm = context.window_manager
|
||||
wm.flamenco_bat_status = "INVESTIGATING"
|
||||
wm.flamenco_can_abort = True # Only BAT v2 can abort.
|
||||
return True
|
||||
|
||||
def bat_v2_packer_finalize_and_quit(self, context: bpy.types.Context) -> set[str]:
|
||||
if self.bat_v2_packer_reported_error:
|
||||
# The errors themselves should have been reported already.
|
||||
context.window_manager.flamenco_bat_status = "ABORTED"
|
||||
return self._quit(context)
|
||||
|
||||
if self.bat_v2_packer is not None:
|
||||
# BAT v2 pack is done.
|
||||
self.blendfile_on_farm = self.bat_v2_packer.blendfile_location_in_pack()
|
||||
|
||||
self._submit_job(context)
|
||||
return self._quit(context)
|
||||
|
||||
# Reporter Protocol for our BAT v2 interface.
|
||||
# See `BATPackReporter` in BAT's `blender_asset_tracer/pack.py`.
|
||||
def on_error_on_error(self, errormsg: str, ex: Exception) -> None:
|
||||
import traceback
|
||||
|
||||
self.bat_v2_packer_reported_error = True
|
||||
|
||||
# This callback is only called on serious errors that likely indicate
|
||||
# bugs, namely when either `on_copy_error()` or `on_rewrite_error()`
|
||||
# caused an exception themselves.
|
||||
print(60 * "-")
|
||||
print("Flamenco ran into an error while sending files to the farm:")
|
||||
print()
|
||||
print(errormsg)
|
||||
print()
|
||||
traceback.print_exception(ex)
|
||||
bug_report_url = "https://flamenco.blender.org/get-involved"
|
||||
print("Please copy-paste the above into a bug report at", bug_report_url)
|
||||
print()
|
||||
print(60 * "-")
|
||||
self.report({"ERROR"}, "Flamenco: Error sending files, check the terminal")
|
||||
|
||||
def on_copy_start(self, src: Path, dest: PurePath) -> None:
|
||||
bpy.context.window_manager.flamenco_bat_status = "TRANSFERRING"
|
||||
self.log.info("Uploading %s", dest)
|
||||
bpy.context.window_manager.flamenco_bat_status_txt = "Uploading {!s}".format(
|
||||
dest.name
|
||||
)
|
||||
|
||||
def on_copy_done(self, src: Path, dest: PurePath) -> None:
|
||||
assert self.bat_v2_packer is not None
|
||||
num_total, num_done = self.bat_v2_packer.num_files_to_transfer()
|
||||
if num_total < 0:
|
||||
progress = 0
|
||||
else:
|
||||
progress = int(100 * num_done / num_total)
|
||||
|
||||
bpy.context.window_manager.flamenco_bat_progress = progress
|
||||
|
||||
def on_copy_error(self, src: Path, dest: PurePath, errormsg: str) -> None:
|
||||
self.bat_v2_packer_reported_error = True
|
||||
self.report({"ERROR"}, "Copying {!s} to {!s}: {!s}".format(src, dest, errormsg))
|
||||
|
||||
def on_rewrite_error(self, blendfile: Path, save_as: Path, errormsg: str) -> None:
|
||||
self.bat_v2_packer_reported_error = True
|
||||
self.report({"ERROR"}, "Rewriting {!s}: {!s}".format(blendfile, errormsg))
|
||||
|
||||
def on_rewrite_start(self, blendfile: Path, save_as: Path) -> None:
|
||||
self.report({"INFO"}, "Rewriting {!s}".format(blendfile.name))
|
||||
wm = bpy.context.window_manager
|
||||
wm.flamenco_bat_status = "REWRITING"
|
||||
wm.flamenco_bat_status_txt = blendfile.name
|
||||
|
||||
def on_rewrite_done(self, blendfile: Path, save_as: Path) -> None:
|
||||
pass
|
||||
|
||||
def on_missing_file(self, blendfile: Path, relpath_in_pack: PurePath) -> None:
|
||||
self.bat_v2_packer_missing_files.append(blendfile)
|
||||
self.report({"WARNING"}, "Missing file: {!s}".format(blendfile))
|
||||
|
||||
# End of Reporter Protocol.
|
||||
|
||||
def _submit_files_bat_v1(self, context: bpy.types.Context, blendfile: Path) -> bool:
|
||||
"""Ensure that the files are somewhere in the shared storage.
|
||||
|
||||
Returns True if a packing thread has been started, and False otherwise.
|
||||
"""
|
||||
|
||||
from .bat import bat_version
|
||||
from .bat import interface as bat_interface
|
||||
|
||||
self.log.info("Submitting files via BAT %s", bat_version())
|
||||
|
||||
if bat_interface.is_packing():
|
||||
self.report({"ERROR"}, "Another packing operation is running")
|
||||
self._quit(context)
|
||||
@@ -517,190 +604,19 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
"File is not already in job storage location, copying it there"
|
||||
)
|
||||
try:
|
||||
self.blendfile_on_farm = self._bat_pack_filesystem(context, blendfile)
|
||||
self.blendfile_on_farm = self._bat_v1_pack_filesystem(
|
||||
context, blendfile
|
||||
)
|
||||
except FileNotFoundError:
|
||||
self._quit(context)
|
||||
return False
|
||||
|
||||
wm = context.window_manager
|
||||
self.timer = wm.event_timer_add(self.TIMER_PERIOD, window=context.window)
|
||||
wm.flamenco_can_abort = False # Only BAT v2 can abort.
|
||||
|
||||
return True
|
||||
|
||||
def _submit_files_bat_bypass(self, context: bpy.types.Context, blendfile: Path) -> bool:
|
||||
"""Bypass BAT: absolute paths, then upload or copy the blend only."""
|
||||
|
||||
manager = self._manager_info(context)
|
||||
if not manager:
|
||||
return False
|
||||
|
||||
self.log.info("Converting all relative paths to absolute")
|
||||
self._convert_relpaths_to_absolute(context)
|
||||
|
||||
self.log.info("Saving blend file with absolute paths")
|
||||
blendfile = self._save_blendfile(context)
|
||||
blendfile = bpathlib.make_absolute(blendfile)
|
||||
|
||||
if manager.shared_storage.shaman_enabled:
|
||||
self.log.info("Uploading blend file directly to Shaman (bypassing BAT)")
|
||||
self._upload_blendfile_to_shaman(context, blendfile)
|
||||
self._quit(context)
|
||||
return False
|
||||
if job_submission.is_file_inside_job_storage(context, blendfile):
|
||||
self.log.info(
|
||||
"File is already in job storage location, submitting it as-is"
|
||||
)
|
||||
self._use_blendfile_directly(context, blendfile)
|
||||
return False
|
||||
self.log.info(
|
||||
"File is not already in job storage location, copying it there"
|
||||
)
|
||||
try:
|
||||
self._copy_blendfile_to_storage(context, blendfile)
|
||||
except FileNotFoundError:
|
||||
self._quit(context)
|
||||
return False
|
||||
return False
|
||||
|
||||
def _upload_blendfile_to_shaman(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> None:
|
||||
"""Upload blend file directly to Shaman without BAT.
|
||||
|
||||
Creates a Shaman checkout with just the blend file, maintaining its
|
||||
relative path from the project root.
|
||||
"""
|
||||
from .bat import cache
|
||||
from .manager.apis import ShamanApi
|
||||
from .manager.models import (
|
||||
ShamanFileSpec,
|
||||
ShamanCheckout,
|
||||
)
|
||||
from .manager.exceptions import ApiException
|
||||
from . import preferences
|
||||
|
||||
api_client = self.get_api_client(context)
|
||||
shaman_api = ShamanApi(api_client)
|
||||
|
||||
# Get project root to calculate relative path
|
||||
prefs = preferences.get(context)
|
||||
project_path: Path = prefs.project_root()
|
||||
project_path = bpathlib.make_absolute(Path(bpy.path.abspath(str(project_path))))
|
||||
|
||||
# Calculate relative path from project root
|
||||
try:
|
||||
blendfile_rel_path = blendfile.relative_to(project_path)
|
||||
# Convert to POSIX path for Shaman
|
||||
blendfile_path_in_checkout = PurePosixPath(blendfile_rel_path.as_posix())
|
||||
except ValueError:
|
||||
# Blend file is not under project root, use just the filename
|
||||
self.log.warning(
|
||||
"Blend file %s is not under project root %s, using filename only",
|
||||
blendfile,
|
||||
project_path,
|
||||
)
|
||||
blendfile_path_in_checkout = PurePosixPath(blendfile.name)
|
||||
|
||||
# Compute checksum and file size
|
||||
self.log.info("Computing checksum for %s", blendfile.name)
|
||||
checksum = cache.compute_cached_checksum(blendfile)
|
||||
filesize = blendfile.stat().st_size
|
||||
|
||||
# Upload the blend file to Shaman
|
||||
self.log.info("Uploading blend file to Shaman: %s", blendfile.name)
|
||||
try:
|
||||
with blendfile.open("rb") as file_reader:
|
||||
shaman_api.shaman_file_store(
|
||||
checksum=checksum,
|
||||
filesize=filesize,
|
||||
body=file_reader,
|
||||
x_shaman_can_defer_upload=True,
|
||||
x_shaman_original_filename=blendfile.name,
|
||||
)
|
||||
except ApiException as ex:
|
||||
if ex.status == 208:
|
||||
# File already known to Shaman
|
||||
self.log.info("Blend file already known to Shaman")
|
||||
elif ex.status == 425:
|
||||
# Defer upload - someone else is uploading
|
||||
self.log.info("Blend file is being uploaded by another client, deferring")
|
||||
# Retry after a short delay
|
||||
import time
|
||||
time.sleep(1)
|
||||
with blendfile.open("rb") as file_reader:
|
||||
shaman_api.shaman_file_store(
|
||||
checksum=checksum,
|
||||
filesize=filesize,
|
||||
body=file_reader,
|
||||
x_shaman_can_defer_upload=False,
|
||||
x_shaman_original_filename=blendfile.name,
|
||||
)
|
||||
else:
|
||||
self.log.error("Error uploading to Shaman: %s", ex)
|
||||
self.report({"ERROR"}, f"Error uploading to Shaman: {ex}")
|
||||
return
|
||||
|
||||
# Create checkout definition with just the blend file
|
||||
checkout_path = self._shaman_checkout_path()
|
||||
filespec = ShamanFileSpec(
|
||||
sha=checksum,
|
||||
size=filesize,
|
||||
path=str(blendfile_path_in_checkout), # Relative path from project root
|
||||
)
|
||||
|
||||
# Create the checkout
|
||||
self.log.info("Creating Shaman checkout: %s", checkout_path)
|
||||
self.log.info("Blend file path in checkout: %s", blendfile_path_in_checkout)
|
||||
checkout = ShamanCheckout(
|
||||
files=[filespec],
|
||||
checkout_path=str(checkout_path),
|
||||
)
|
||||
|
||||
try:
|
||||
result = shaman_api.shaman_checkout(checkout)
|
||||
self.actual_shaman_checkout_path = PurePosixPath(result.checkout_path)
|
||||
# The checkout itself is created in a unique subdirectory. The job's
|
||||
# blendfile must include that checkout path.
|
||||
self.blendfile_on_farm = (
|
||||
PurePosixPath("{jobs}")
|
||||
/ self.actual_shaman_checkout_path
|
||||
/ blendfile_path_in_checkout
|
||||
)
|
||||
self.log.info("Shaman checkout created: %s", self.actual_shaman_checkout_path)
|
||||
self._submit_job(context)
|
||||
except ApiException as ex:
|
||||
self.log.error("Error creating Shaman checkout: %s", ex)
|
||||
self.report({"ERROR"}, f"Error creating Shaman checkout: {ex}")
|
||||
return
|
||||
|
||||
def _copy_blendfile_to_storage(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> None:
|
||||
"""Copy blend file to job storage without BAT."""
|
||||
import shutil
|
||||
|
||||
manager = self._manager_info(context)
|
||||
if not manager:
|
||||
raise FileNotFoundError("Manager info not known")
|
||||
|
||||
unique_dir = "%s-%s" % (
|
||||
datetime.datetime.now().isoformat("-").replace(":", ""),
|
||||
self.job_name,
|
||||
)
|
||||
pack_target_dir = Path(manager.shared_storage.location) / unique_dir
|
||||
pack_target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
pack_target_file = pack_target_dir / blendfile.name
|
||||
self.log.info("Copying blend file to %s", pack_target_file)
|
||||
|
||||
shutil.copy2(blendfile, pack_target_file)
|
||||
|
||||
self.blendfile_on_farm = PurePosixPath(pack_target_file.as_posix())
|
||||
self.actual_shaman_checkout_path = None
|
||||
|
||||
self._submit_job(context)
|
||||
|
||||
def _bat_pack_filesystem(
|
||||
def _bat_v1_pack_filesystem(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> PurePosixPath:
|
||||
"""Use BAT to store the pack on the filesystem.
|
||||
@@ -709,7 +625,9 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
"""
|
||||
from .bat import interface as bat_interface
|
||||
|
||||
project_path = self._bat_project_path(context, blendfile)
|
||||
# Get project path from addon preferences.
|
||||
prefs = preferences.get(context)
|
||||
project_path: Path = prefs.project_root()
|
||||
project_path = bpathlib.make_absolute(Path(bpy.path.abspath(str(project_path))))
|
||||
|
||||
if not project_path.exists():
|
||||
@@ -734,10 +652,8 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
base_blendfile=blendfile,
|
||||
project=project_path,
|
||||
target=str(pack_target_dir),
|
||||
exclusion_filter="", # TODO: get from GUI.
|
||||
# False: relative_only=True can leave linked blends on KEEP_PATH and hit BAT
|
||||
# _rewrite_paths assertions with Shaman (same as stock 3.8.x + BAT 1.x).
|
||||
relative_only=False,
|
||||
exclusion_filter=prefs.exclusion_filter,
|
||||
relative_only=prefs.use_relative_only,
|
||||
)
|
||||
|
||||
return PurePosixPath(pack_target_file.as_posix())
|
||||
@@ -762,21 +678,23 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
"""
|
||||
from .bat import (
|
||||
interface as bat_interface,
|
||||
)
|
||||
from .bat import (
|
||||
shaman as bat_shaman,
|
||||
)
|
||||
|
||||
assert self.job is not None
|
||||
self.log.info("Sending BAT pack to Shaman")
|
||||
|
||||
project_path = self._bat_project_path(context, blendfile)
|
||||
prefs = preferences.get(context)
|
||||
project_path: Path = prefs.project_root()
|
||||
|
||||
self.packthread = bat_interface.copy(
|
||||
base_blendfile=blendfile,
|
||||
project=project_path,
|
||||
target="/", # Target directory irrelevant for Shaman transfers.
|
||||
exclusion_filter="", # TODO: get from GUI.
|
||||
# See _bat_pack_filesystem: avoid BAT+Shaman KEEP_PATH / _rewrite_paths failure.
|
||||
relative_only=False,
|
||||
exclusion_filter=prefs.exclusion_filter,
|
||||
relative_only=prefs.use_relative_only,
|
||||
packer_class=bat_shaman.Packer,
|
||||
packer_kwargs=dict(
|
||||
api_client=self.get_api_client(context),
|
||||
@@ -821,10 +739,6 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
def _use_blendfile_directly(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> None:
|
||||
# The temporary '.flamenco.blend' file should not be deleted, as it
|
||||
# will be used directly by the render job.
|
||||
self.temp_blendfile = None
|
||||
|
||||
# The blend file is contained in the job storage path, no need to
|
||||
# copy anything.
|
||||
self.blendfile_on_farm = bpathlib.make_absolute(blendfile)
|
||||
@@ -872,10 +786,13 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
if not self._prepare_job_for_submission(context):
|
||||
return
|
||||
|
||||
context.window_manager.flamenco_bat_status = "COMMUNICATING"
|
||||
context.window_manager.flamenco_bat_status_txt = ""
|
||||
|
||||
api_client = self.get_api_client(context)
|
||||
try:
|
||||
submitted_job = job_submission.submit_job(self.job, api_client)
|
||||
except MaxRetryError as ex:
|
||||
except MaxRetryError:
|
||||
self.report({"ERROR"}, "Unable to reach Flamenco Manager")
|
||||
return
|
||||
except HTTPError as ex:
|
||||
@@ -895,7 +812,27 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
self.report({"ERROR"}, f"Could not submit job: {ex.reason}")
|
||||
return
|
||||
|
||||
self.report({"INFO"}, "Job %s submitted" % submitted_job.name)
|
||||
# Show a final report.
|
||||
if self.bat_v2_packer:
|
||||
num_missing_files = len(self.bat_v2_packer_missing_files)
|
||||
else:
|
||||
# Only BATv2 tracks missing files like this.
|
||||
num_missing_files = 0
|
||||
if num_missing_files:
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
"Job {!s} submitted (with {:d} missing files)".format(
|
||||
submitted_job.name, num_missing_files
|
||||
),
|
||||
)
|
||||
context.window_manager.flamenco_bat_status_txt = (
|
||||
"Submitted with {:d} missing files".format(num_missing_files)
|
||||
)
|
||||
else:
|
||||
self.report({"INFO"}, "Job {!s} submitted".format(submitted_job.name))
|
||||
context.window_manager.flamenco_bat_status_txt = ""
|
||||
|
||||
context.window_manager.flamenco_bat_status = "DONE"
|
||||
|
||||
def _check_job(self, context: bpy.types.Context) -> bool:
|
||||
"""Use the Flamenco API to check the Job before submitting files.
|
||||
@@ -911,7 +848,7 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
api_client = self.get_api_client(context)
|
||||
try:
|
||||
job_submission.submit_job_check(self.job, api_client)
|
||||
except MaxRetryError as ex:
|
||||
except MaxRetryError:
|
||||
self.report({"ERROR"}, "Unable to reach Flamenco Manager")
|
||||
return False
|
||||
except HTTPError as ex:
|
||||
@@ -938,9 +875,10 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
Does neither check nor abort the BAT pack thread.
|
||||
"""
|
||||
|
||||
if self.temp_blendfile is not None:
|
||||
self.log.info("Removing temporary file %s", self.temp_blendfile)
|
||||
self.temp_blendfile.unlink(missing_ok=True)
|
||||
if self.bat_v2_packer is not None and not self.bat_v2_packer.is_done:
|
||||
self.log.info("Aborting BAT packer")
|
||||
self.bat_v2_packer.abort()
|
||||
self.bat_v2_packer = None
|
||||
|
||||
if self.timer is not None:
|
||||
context.window_manager.event_timer_remove(self.timer)
|
||||
@@ -948,6 +886,31 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class FLAMENCO_OT_abort(bpy.types.Operator):
|
||||
bl_idname = "flamenco.abort"
|
||||
bl_label = "Abort"
|
||||
bl_description = (
|
||||
"Abort a running job submission.\nBlender make take a while to respond to this"
|
||||
)
|
||||
|
||||
ABORTABLE_STATES = {
|
||||
"INVESTIGATING",
|
||||
"REWRITING",
|
||||
"TRANSFERRING",
|
||||
"COMMUNICATING",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
wm = context.window_manager
|
||||
return wm.flamenco_can_abort and wm.flamenco_bat_status in cls.ABORTABLE_STATES
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> set[str]:
|
||||
wm = context.window_manager
|
||||
wm.flamenco_bat_status = "ABORTING"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class FLAMENCO3_OT_explore_file_path(bpy.types.Operator):
|
||||
"""Opens the given path in a file explorer.
|
||||
|
||||
@@ -963,8 +926,8 @@ class FLAMENCO3_OT_explore_file_path(bpy.types.Operator):
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
import platform
|
||||
import pathlib
|
||||
import platform
|
||||
|
||||
# Possibly open a parent of the path
|
||||
to_open = pathlib.Path(self.path)
|
||||
@@ -1001,6 +964,7 @@ classes = (
|
||||
FLAMENCO_OT_ping_manager,
|
||||
FLAMENCO_OT_eval_setting,
|
||||
FLAMENCO_OT_submit_job,
|
||||
FLAMENCO_OT_abort,
|
||||
FLAMENCO3_OT_explore_file_path,
|
||||
)
|
||||
register, unregister = bpy.utils.register_classes_factory(classes)
|
||||
|
||||
Reference in New Issue
Block a user