work: restore shift+spacebar for media play/pause

maybe put in maya config? idk what funiman's preference is
This commit is contained in:
Nathan
2026-05-29 14:58:59 -06:00
parent 2f8e5f472f
commit 6c3b78075b
130 changed files with 10461 additions and 19696 deletions
+346 -382
View File
@@ -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)