diff --git a/addon/flamenco/__init__.py b/addon/flamenco/__init__.py index 8b7131e6..eb31d45f 100644 --- a/addon/flamenco/__init__.py +++ b/addon/flamenco/__init__.py @@ -37,6 +37,8 @@ def discard_global_flamenco_data(_): job_types.discard_flamenco_data() comms.discard_flamenco_data() + bpy.context.WindowManager.flamenco_version_mismatch = False + def redraw(self, context): if context.area is None: @@ -118,6 +120,11 @@ def register() -> None: max=100, update=redraw, ) + bpy.types.WindowManager.flamenco_version_mismatch = bpy.props.BoolProperty( + name="Flamenco Ignore Version Mismatch", + default=False, + description="Ignore version mismatch between add-on and Manager when submitting a job", + ) # Placeholder to contain the result of a 'ping' to Flamenco Manager, # so that it can be shown in the preferences panel. diff --git a/addon/flamenco/comms.py b/addon/flamenco/comms.py index 2948aeb7..d69e5b9e 100644 --- a/addon/flamenco/comms.py +++ b/addon/flamenco/comms.py @@ -3,6 +3,7 @@ # import logging +import dataclasses from typing import TYPE_CHECKING, Optional from urllib3.exceptions import HTTPError, MaxRetryError @@ -25,6 +26,23 @@ else: _ManagerConfiguration = object +@dataclasses.dataclass(frozen=True) +class ManagerInfo: + version: Optional[_FlamencoVersion] = None + config: Optional[_ManagerConfiguration] = None + error: str = "" + + @classmethod + def with_error(cls, error: str) -> "ManagerInfo": + return cls(error=error) + + @classmethod + def with_info( + cls, version: _FlamencoVersion, config: _ManagerConfiguration + ) -> "ManagerInfo": + return cls(version=version, config=config) + + def flamenco_api_client(manager_url: str) -> _ApiClient: """Returns an API client for communicating with a Manager.""" global _flamenco_client @@ -45,6 +63,18 @@ def flamenco_api_client(manager_url: str) -> _ApiClient: return _flamenco_client +def flamenco_client_version() -> str: + """Return the version of the Flamenco OpenAPI client.""" + + from . import dependencies + + dependencies.preload_modules() + + from . import manager + + return manager.__version__ + + def discard_flamenco_data(): global _flamenco_client @@ -57,7 +87,9 @@ def discard_flamenco_data(): def ping_manager_with_report( - context: bpy.types.Context, api_client: _ApiClient, prefs: _FlamencoPreferences + window_manager: bpy.types.WindowManager, + api_client: _ApiClient, + prefs: _FlamencoPreferences, ) -> tuple[str, str]: """Ping the Manager, update preferences, and return a report as string. @@ -67,26 +99,23 @@ def ping_manager_with_report( `Operator.report()`. """ - context.window_manager.flamenco_status_ping = "..." + info = ping_manager(window_manager, api_client, prefs) + if info.error: + return info.error, "ERROR" - version, _, err = ping_manager(api_client, prefs) - if err: - context.window_manager.flamenco_status_ping = err - return err, "ERROR" - - assert version is not None - report = "%s version %s found" % (version.name, version.version) - context.window_manager.flamenco_status_ping = report + assert info.version is not None + report = "%s version %s found" % (info.version.name, info.version.version) return report, "INFO" def ping_manager( - api_client: _ApiClient, prefs: _FlamencoPreferences -) -> tuple[Optional[_FlamencoVersion], Optional[_ManagerConfiguration], str]: - """Fetch Manager config & version, and update preferences. + window_manager: bpy.types.WindowManager, + api_client: _ApiClient, + prefs: _FlamencoPreferences, +) -> ManagerInfo: + """Fetch Manager config & version, and update cached preferences.""" - :returns: tuple (version, config, error). - """ + window_manager.flamenco_status_ping = "..." # Do a late import, so that the API is only imported when actually used. from flamenco.manager import ApiException @@ -94,21 +123,29 @@ def ping_manager( from flamenco.manager.models import FlamencoVersion, ManagerConfiguration meta_api = MetaApi(api_client) + error = "" try: version: FlamencoVersion = meta_api.get_version() config: ManagerConfiguration = meta_api.get_configuration() except ApiException as ex: - return (None, None, "Manager cannot be reached: %s" % ex) + error = "Manager cannot be reached: %s" % ex except MaxRetryError as ex: # This is the common error, when for example the port number is # incorrect and nothing is listening. The exception text is not included # because it's very long and confusing. - return (None, None, "Manager cannot be reached") + error = "Manager cannot be reached" except HTTPError as ex: - return (None, None, "Manager cannot be reached: %s" % ex) + error = "Manager cannot be reached: %s" % ex + + if error: + window_manager.flamenco_status_ping = error + return ManagerInfo.with_error(error) # Store whether this Manager supports the Shaman API. prefs.is_shaman_enabled = config.shaman_enabled prefs.job_storage = config.storage_location - return version, config, "" + report = "%s version %s found" % (version.name, version.version) + window_manager.flamenco_status_ping = report + + return ManagerInfo.with_info(version, config) diff --git a/addon/flamenco/gui.py b/addon/flamenco/gui.py index b0ccad88..449c51cc 100644 --- a/addon/flamenco/gui.py +++ b/addon/flamenco/gui.py @@ -133,13 +133,7 @@ class FLAMENCO_PT_job_submission(bpy.types.Panel): # Show current status of Flamenco. flamenco_status = context.window_manager.flamenco_bat_status if flamenco_status in {"IDLE", "ABORTED", "DONE"}: - ui = layout - props = ui.operator( - "flamenco.submit_job", - text="Submit to Flamenco", - icon="RENDER_ANIMATION", - ) - props.job_name = context.scene.flamenco_job_name + self.draw_submit_button(context, layout) elif flamenco_status == "INVESTIGATING": row = layout.row(align=True) row.label(text="Investigating your files") @@ -163,6 +157,28 @@ class FLAMENCO_PT_job_submission(bpy.types.Panel): ): layout.label(text=context.window_manager.flamenco_bat_status_txt) + def draw_submit_button( + self, context: bpy.types.Context, layout: bpy.types.UILayout + ) -> None: + row = layout.row(align=True) + + props = row.operator( + "flamenco.submit_job", + text="Submit to Flamenco", + icon="RENDER_ANIMATION", + ) + props.job_name = context.scene.flamenco_job_name + props.ignore_version_mismatch = False + + if context.window_manager.flamenco_version_mismatch: + props = row.operator( + "flamenco.submit_job", + text="Force Submit", + icon="NONE", + ) + props.job_name = context.scene.flamenco_job_name + props.ignore_version_mismatch = True + classes = (FLAMENCO_PT_job_submission,) register, unregister = bpy.utils.register_classes_factory(classes) diff --git a/addon/flamenco/operators.py b/addon/flamenco/operators.py index 830a37ba..a0916f1b 100644 --- a/addon/flamenco/operators.py +++ b/addon/flamenco/operators.py @@ -86,7 +86,9 @@ class FLAMENCO_OT_ping_manager(FlamencoOpMixin, bpy.types.Operator): api_client = self.get_api_client(context) prefs = preferences.get(context) - report, level = comms.ping_manager_with_report(context, api_client, prefs) + report, level = comms.ping_manager_with_report( + context.window_manager, api_client, prefs + ) self.report({level}, report) return {"FINISHED"} @@ -122,6 +124,10 @@ 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: Optional[bpy.types.Timer] = None packthread: Optional[_PackThread] = None @@ -135,6 +141,16 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): return job_type is not None def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]: + # Before doing anything, make sure the info we cached about the Manager + # is up to date. A change in job storage directory on the Manager can + # cause nasty error messages when we submit, and it's better to just be + # ahead of the curve and refresh first. This also allows for checking + # the actual Manager version before submitting. + err = self._check_manager(context) + if err: + self.report({"WARNING"}, err) + return {"CANCELLED"} + filepath = self._save_blendfile(context) # Construct the Job locally before trying to pack. If any validations fail, better fail early. @@ -160,6 +176,51 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): return self._on_bat_pack_msg(context, msg) + def _check_manager(self, context: bpy.types.Context) -> str: + """Check the Manager version & fetch the job storage directory. + + :return: an error string when something went wrong. + """ + from . import comms, preferences + + # Get the manager's info. This is cached in the preferences, so + # regardless of whether this function actually responds to version + # mismatches, it has to be called to also refresh the shared storage + # location. + api_client = self.get_api_client(context) + prefs = preferences.get(context) + mgrinfo = comms.ping_manager(context.window_manager, api_client, prefs) + if mgrinfo.error: + return mgrinfo.error + + # Check the Manager's version. + if not self.ignore_version_mismatch: + my_version = comms.flamenco_client_version() + assert mgrinfo.version is not None + + try: + mgrversion = mgrinfo.version.shortversion + except AttributeError: + # shortversion was introduced in Manager version 3.0-beta2, which + # may not be running here yet. + mgrversion = mgrinfo.version.version + if mgrversion != my_version: + context.window_manager.flamenco_version_mismatch = True + return ( + f"Manager ({mgrversion}) and this add-on ({my_version}) version " + + "mismatch, either update the add-on or force the submission" + ) + + # Un-set the 'flamenco_version_mismatch' when the versions match or when + # one forced submission is done. Each submission has to go through the + # same cycle of submitting, seeing the warning, then explicitly ignoring + # the mismatch, to make it a concious decision to keep going with + # potentially incompatible versions. + context.window_manager.flamenco_version_mismatch = False + + # Empty error message indicates 'ok'. + return "" + def _save_blendfile(self, context): """Save to a different file, specifically for Flamenco.