Skip to content

feat: Run tool/engine in background thread #297

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -173,18 +173,24 @@ def registry(self):
'''Return registry object'''
return self._registry

@property
def worker(self):
'''Return background worker thread class'''
return self._worker

@property
def tool_config_options(self):
return self._tool_config_options

def __init__(
self,
event_manager,
registry,
):
def __init__(self, event_manager, registry, worker=None):
'''
Initialise Client with instance of
:class:`~ftrack_framework_core.event.EventManager`
:class:`~ftrack_framework_core.event.EventManager` and instance of
:class:`~ftrack_framework_core.registry.Registry`, with optional background
thread *worker* class.

The worker class must take method and arguments on initialization and
have a run method to execute the given method with the given arguments.
'''
# TODO: double check logger initialization and standardize it around all files.
# Setting logger
Expand All @@ -198,8 +204,13 @@ def __init__(
# Set the event manager
self._event_manager = event_manager

# Setting init variables to 0
# Set the registry
self._registry = registry

# Set the worker
self._worker = worker

# Setting init variables to 0
self._host_context_changed_subscribe_id = None
self.__instanced_dialogs = {}
self._dialog = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,3 +468,15 @@ class is not registrable.
}

return data

def _run_with_worker(self, target, args=None, kwargs=None):
'''Run the given *target* method with a background thread worker,
if client has been initialized with one. Enables running a tool in a background thread,
enabling real time progress updates.'''
worker_class = self.client_property_getter_connection('worker')
if worker_class:
worker = worker_class(target, args=args, kwargs=kwargs)
worker.start()
return worker
else:
target(*args, **kwargs)
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# :coding: utf-8
# :copyright: Copyright (c) 2014-2023 ftrack
import copy

from Qt import QtWidgets, QtCore

Expand All @@ -11,6 +10,7 @@
from ftrack_qt.widgets.selectors import ContextSelector

from ftrack_qt.utils.layout import recursive_clear_layout
from ftrack_qt.utils.decorators import invoke_in_qt_main_thread


class BaseContextDialog(FrameworkDialog, StyledDialog):
Expand Down Expand Up @@ -140,7 +140,15 @@ def post_build(self):
self._on_context_selected_callback
)
# Connect run_tool_config button
self._run_button.clicked.connect(self._on_run_button_clicked)
self._run_button.clicked.connect(self._on_run_button_clicked_sync)

@invoke_in_qt_main_thread
def rebuild_ui(self):
'''Rebuild the UI'''
self.clean_ui()
self.pre_build_ui()
self.build_ui()
self.post_build_ui()

def _on_context_selected_callback(self, context_id):
'''Emit signal with the new context_id'''
Expand All @@ -156,17 +164,22 @@ def _on_client_context_changed_callback(self, event=None):
self.selected_context_id = self.context_id
# Clean the UI every time a new context is set as the current tool
# config might not be available anymore
self.clean_ui()
self.pre_build_ui()
self.build_ui()
self.post_build_ui()
self.rebuild_ui()

def _on_run_button_clicked_sync(self):
'''Run button clicked, enable running tool in background thread if
client has a Worker class defined. Otherwise run in the main thread.'''

# Always store worker in self._worker to avoid garbage collection while
# thread is running
self._worker = self._run_with_worker(self._on_run_button_clicked)

def _on_run_button_clicked(self):
'''
Run button from the UI has been clicked.
Tell client to run the current tool config
Tell client to run the current tool config. This method might execute
in a background thread.
'''

self.run_tool_config(self.tool_config['reference'])

# FrameworkDialog overrides
Expand Down
52 changes: 52 additions & 0 deletions libs/qt/source/ftrack_qt/utils/threading/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,61 @@
# :coding: utf-8
# :copyright: Copyright (c) 2014-2023 ftrack
import sys

from Qt import QtCore, QtWidgets


class Worker(QtCore.QThread):
'''Perform work in a background thread.'''

def __init__(self, function, args=None, kwargs=None, parent=None):
'''Execute *function* in separate thread.

*args* should be a list of positional arguments and *kwargs* a
mapping of keyword arguments to pass to the function on execution.

Store function call as self.result. If an exception occurs
store as self.error.

Example::

try:
worker = Worker(theQuestion, [42])
worker.start()

while worker.isRunning():
app = QtGui.QApplication.instance()
app.processEvents()

if worker.error:
raise worker.error[1], None, worker.error[2]

except Exception as error:
traceback.print_exc()
QtGui.QMessageBox.critical(
None,
'Error',
'An unhandled error occurred:'
'\\n{0}'.format(error)
)

'''
super(Worker, self).__init__(parent=parent)
self.setObjectName(str(function))
self.function = function
self.args = args or []
self.kwargs = kwargs or {}
self.result = None
self.error = None

def run(self):
'''Execute function and store result.'''
try:
self.result = self.function(*self.args, **self.kwargs)
except Exception:
self.error = sys.exc_info()


class InvokeEvent(QtCore.QEvent):
'''Event.'''

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ProgressStatusButtonWidget,
ProgressPhaseButtonWidget,
)
from ftrack_qt.utils.decorators import invoke_in_qt_main_thread


class ProgressWidget(QtWidgets.QWidget):
Expand Down Expand Up @@ -169,6 +170,7 @@ def reset_statuses(self, new_status=None, status_message=''):
phase_widget.update_status(new_status, status_message, None)
phase_widget.hide_log()

@invoke_in_qt_main_thread
def run(self, main_widget):
'''Run progress widget on top of *main_widget*, with *action*'''
self.reset_statuses()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ def post_build_ui(self):
pass

def _on_run_button_clicked(self):
'''(Override) Drive the progress widget'''
'''(Override) Run open tool, and drive the progress widget. Might be run in
a background thread.'''
self._progress_widget.run(self)
super(StandardOpenerDialog, self)._on_run_button_clicked()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,12 @@ def post_build_ui(self):
pass

def _on_run_button_clicked(self):
'''(Override) Refresh context widget(s) upon publish'''
'''(Override) Publish, followed by a refresh of the UI. Might be run in
a background thread.'''
self._progress_widget.run(self)
super(StandardPublisherDialog, self)._on_run_button_clicked()
# TODO: This will not work in remote mode (async mode) as plugin events
# will arrive after this point of execution.
if self._progress_widget.status == constants.status.SUCCESS_STATUS:
self.clean_ui()
self.pre_build_ui()
self.build_ui()
self.post_build_ui()
self.rebuild_ui()

@invoke_in_qt_main_thread
def plugin_run_callback(self, log_item):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ftrack_framework_core.configure_logging import configure_logging

from ftrack_qt.utils.decorators import invoke_in_qt_main_thread
from ftrack_qt.utils.threading import Worker

from .rpc_cep import PhotoshopRPCCEP
from . import process_util
Expand Down Expand Up @@ -81,7 +82,7 @@ def bootstrap_integration(framework_extensions_path):

Host(event_manager, registry=registry_instance)

client = Client(event_manager, registry=registry_instance)
client = Client(event_manager, registry=registry_instance, worker=Worker)

# Init tools
dcc_config = registry_instance.get_one(
Expand Down
5 changes: 4 additions & 1 deletion tests/framework/manual/standalone_ui_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

from ftrack_framework_core import host, event, registry
import ftrack_constants.framework as constants
from ftrack_qt.utils.threading import Worker

session = ftrack_api.Session(auto_connect_event_hub=False)
event_manager = event.EventManager(
Expand All @@ -60,7 +61,9 @@

from ftrack_framework_core import client

client_class = client.Client(event_manager, registry=registry_instance)
client_class = client.Client(
event_manager, registry=registry_instance, worker=Worker
)

app = QtWidgets.QApplication.instance()
if not app:
Expand Down