diff --git a/build_resources.yml b/build_resources.yml index af26f47dab..c3cb449a34 100644 --- a/build_resources.yml +++ b/build_resources.yml @@ -22,3 +22,4 @@ - ui_busy_dialog res_files: - resources + import_pattern: . diff --git a/python/tank/authentication/console_authentication.py b/python/tank/authentication/console_authentication.py index 679a6aa46a..db292f1cf1 100644 --- a/python/tank/authentication/console_authentication.py +++ b/python/tank/authentication/console_authentication.py @@ -75,8 +75,7 @@ def authenticate(self, hostname, login, http_proxy): hostname = sanitize_url(hostname) - site_i = site_info.SiteInfo() - site_i.reload(hostname, http_proxy) + site_i = site_info.get(hostname, http_proxy=http_proxy) if not site_i.app_session_launcher_enabled: # Will raise an exception if using a username/password pair is diff --git a/python/tank/authentication/defaults_manager.py b/python/tank/authentication/defaults_manager.py index 886b5c11cb..b9b7cc00bd 100644 --- a/python/tank/authentication/defaults_manager.py +++ b/python/tank/authentication/defaults_manager.py @@ -39,6 +39,8 @@ def __init__(self, fixed_host=None): self._user_settings = UserSettings() self._system_settings = SystemSettings() self._fixed_host = fixed_host + self._host = False # current value might be None + self._login = False # current value might be None def is_host_fixed(self): """ @@ -78,11 +80,15 @@ def get_host(self): :returns: A string containing the default host name. """ - return ( - self._fixed_host - or session_cache.get_current_host() - or self._user_settings.default_site - ) + + if self._host is False: + self._host = ( + self._fixed_host + or session_cache.get_current_host() + or self._user_settings.default_site + ) + + return self._host def set_host(self, host): """ @@ -94,6 +100,9 @@ def set_host(self, host): if self.is_host_fixed(): return session_cache.set_current_host(host) + self._host = host + # Reset login + self._login = False def get_http_proxy(self): """ @@ -130,13 +139,17 @@ def get_login(self): """ # Make sure there is a current host. There could be none if no-one has # logged in with Toolkit yet. - if self.get_host(): - return ( - session_cache.get_current_user(self.get_host()) - or self._user_settings.default_login - ) - else: - return self._user_settings.default_login + + if self._login is False: + if self.get_host(): + self._login = ( + session_cache.get_current_user(self.get_host()) + or self._user_settings.default_login + ) + else: + self._login = self._user_settings.default_login + + return self._login def get_user_credentials(self): """ @@ -163,8 +176,6 @@ def get_user_credentials(self): """ if self.get_host() and self.get_login(): return session_cache.get_session_data(self.get_host(), self.get_login()) - else: - return None def set_login(self, login): """ diff --git a/python/tank/authentication/login_dialog.py b/python/tank/authentication/login_dialog.py index fd24529620..6e7cfac5fb 100644 --- a/python/tank/authentication/login_dialog.py +++ b/python/tank/authentication/login_dialog.py @@ -40,7 +40,7 @@ qt_version_tuple, ) from . import app_session_launcher -from . import site_info +from . import site_info as sg_site_info from .sso_saml2 import ( SsoSaml2IncompletePySide2, SsoSaml2Toolkit, @@ -91,28 +91,30 @@ class QuerySiteAndUpdateUITask(QtCore.QThread): to avoid blocking the main GUI thread. """ - def __init__(self, parent, site_info_instance, http_proxy=None): - """ - Constructor. - """ - QtCore.QThread.__init__(self, parent) - self._site_info = site_info_instance - self._http_proxy = http_proxy - - @property - def url_to_test(self): - """String R/W property.""" - return self._url_to_test - - @url_to_test.setter - def url_to_test(self, value): - self._url_to_test = value + succeed = QtCore.Signal(str, sg_site_info.SiteInfo) + failed = QtCore.Signal(str, Exception) + def __init__(self, url, http_proxy=None, parent=None): + super().__init__(parent=parent) + self._url = url + self._http_proxy = http_proxy + def run(self): """ Runs the thread. """ - self._site_info.reload(self._url_to_test, self._http_proxy) + + logger.info(f"QuerySiteAndUpdateUITask::run start for {self._url}") + + try: + info = sg_site_info.get(self._url, http_proxy=self._http_proxy) + except Exception as exc: + logger.error(f"Failed to get site info for {self._url}: {exc}") + self.failed.emit(self._url, exc) + else: + logger.info(f"Successfully got site info for {self._url}") + self.succeed.emit(self._url, info) + class LoginDialog(QtGui.QDialog): """ @@ -200,16 +202,16 @@ def __init__( self.ui.login.set_style_sheet(completer_style) self.ui.login.set_placeholder_text("login") - self._populate_user_dropdown(recent_hosts[0] if recent_hosts else None) + if recent_hosts: + self._populate_user_dropdown(recent_hosts[0]) # Timer to update the GUI according to the URL. # This is to make the UX smoother, as we do not check after each character # typed, but instead wait for a period of inactivity from the user. self._url_changed_timer = QtCore.QTimer(self) + self._url_changed_timer.setInterval(USER_INPUT_DELAY_BEFORE_SITE_INFO_REQUEST) self._url_changed_timer.setSingleShot(True) - self._url_changed_timer.timeout.connect( - self._update_ui_according_to_site_support - ) + self._url_changed_timer.timeout.connect(self._on_site_changed) # If the host is fixed, disable the site textbox. if fixed_host: @@ -306,19 +308,8 @@ def __init__( self.ui.site.activated.connect(self._on_site_changed) self.ui.site.lineEdit().editingFinished.connect(self._on_site_changed) - self.site_info = site_info.SiteInfo() - - self._query_task = QuerySiteAndUpdateUITask(self, self.site_info, http_proxy) - self._query_task.finished.connect(self._toggle_web) - self._update_ui_according_to_site_support() - - # We want to wait until we know what is supported by the site, to avoid - # flickering GUI. - if not self._query_task.wait(THREAD_WAIT_TIMEOUT_MS): - logger.warning( - "Timed out awaiting requesting information: %s" - % self._get_current_site() - ) + self._query_task = None # Will be created when needed + self._http_proxy = http_proxy # Initialize exit confirm message box self.confirm_box = QtGui.QMessageBox( @@ -343,14 +334,32 @@ def __init__( "will result in canceling your request." ) + self.site_info = None + self.confirm_box.setStyleSheet(self.styleSheet()) + # Init UI Spinner + self.ui.spinner_movie = QtGui.QMovie(":/spinning_wheel.gif") + if self.ui.spinner_movie.loopCount() != -1: + self.ui.spinner_movie.finished.connect(self.ui.spinner_movie.start) + # TODO get a better GIF that loop forever + + self.ui.refresh_site_info_spinner.setMovie(self.ui.spinner_movie) + + # TODO: Init which widget are display by default + # IF not host at all in the recent box: nothing but the site widget + # IF host exists: spinner mode + + self._on_site_changed() # trigger the first site info request + + def __del__(self): """ Destructor. """ # We want to clean up any running qthread. - self._query_task.wait() + if self._query_task: + self._query_task.wait() def _confirm_exit(self): return self.confirm_box.exec_() == QtGui.QMessageBox.StandardButton.Yes @@ -362,6 +371,10 @@ def closeEvent(self, event): event.ignore() return + # Stop the query thread if running + if self._query_task and self._query_task.isRunning(): + self._query_task.wait() + if self._asl_task: self._asl_task.finished.disconnect(self._asl_task_finished) self._asl_task.stop_when_possible() @@ -376,6 +389,10 @@ def keyPressEvent(self, event): event.ignore() return + # Stop the query thread if running + if self._query_task and self._query_task.isRunning(): + self._query_task.wait() + if self._asl_task: self._asl_task.finished.disconnect(self._asl_task_finished) self._asl_task.stop_when_possible() @@ -390,9 +407,7 @@ def _get_current_site(self): :returns: The site to connect to. """ - return sgutils.ensure_str( - connection.sanitize_url(self.ui.site.currentText().strip()) - ) + return self.host_selected def _get_current_user(self): """ @@ -402,40 +417,114 @@ def _get_current_user(self): """ return sgutils.ensure_str(self.ui.login.currentText().strip()) - def _update_ui_according_to_site_support(self): - """ - Updates the GUI according to the site's information, hiding or showing - the username/password fields. - """ - self._query_task.url_to_test = self._get_current_site() - self._query_task.start() - def _site_url_changing(self, text): """ Starts a timer to wait until the user stops entering the URL . """ - self._url_changed_timer.start(USER_INPUT_DELAY_BEFORE_SITE_INFO_REQUEST) + self._url_changed_timer.start() + # If the timer is already running, it will be stopped and restarted def _on_site_changed(self): """ Called when the user is done editing the site. It will refresh the list of recent users. """ - self.ui.login.clear() - self._populate_user_dropdown(self._get_current_site()) - self._update_ui_according_to_site_support() - def _populate_user_dropdown(self, site): + host_selected = connection.sanitize_url(self.ui.site.currentText()) + if host_selected == self.host_selected: + logger.debug(f"_on_site_changed - site has not changed: {host_selected}") + # No change, nothing to do. + return + + self.host_selected = host_selected + self.site_info = sg_site_info.get(host_selected, cache_only=True) + + self._populate_user_dropdown(host_selected) + + if self.site_info: + self._update_login_page_ui() + return + + self.ui.login.setVisible(False) + self.ui.password.setVisible(False) + self.ui.message.setVisible(False) + self.ui.forgot_password_link.setVisible(False) + self.ui.button_options.setVisible(False) + self.ui.sign_in.setVisible(False) # maybe setEnable instead? + + self.ui.refresh_site_info_spinner.setVisible(True) + self.ui.refresh_site_info_label.setVisible(True) + + logger.debug("Creating new thread for site info request") + + # Stop any existing thread first + if self._query_task and self._query_task.isRunning(): + self._query_task.wait() + + # Create a new thread for this request + self._query_task = QuerySiteAndUpdateUITask( + url=self._get_current_site(), + http_proxy=self._http_proxy, + parent=self + ) + + # Connect signals for this new thread + self._query_task.succeed.connect(self._on_site_info_response) + self._query_task.failed.connect(self._on_site_info_failure) + + # Make sure signals are delivered to main thread + self._query_task.finished.connect(lambda: logger.debug("Thread finished")) + + self._query_task.start() + + logger.debug(f"Thread started for URL: {self._get_current_site()}") + + def _on_site_info_response(self, site_url: str, site_info): + logger.info(f"_on_site_info_response - site_url: {site_url}") + logger.debug(f"_on_site_info_response - current site: {self._get_current_site()}") + + if self._get_current_site() != site_url: + logger.debug("_on_site_info_response - site-info thread finished too late, we already selected another host") + return + + self.site_info = site_info + + logger.debug("_on_site_info_response - hiding spinner and showing UI elements") + self.ui.refresh_site_info_spinner.setVisible(False) + self.ui.refresh_site_info_label.setVisible(False) + self.ui.message.setVisible(True) + self.ui.sign_in.setVisible(True) + + logger.debug("_on_site_info_response - calling _update_login_page_ui") + self._update_login_page_ui() + + def _on_site_info_failure(self, site_url: str, exc: Exception): + logger.info(f"_on_site_info_failure - site_url: {site_url}; response: {exc}") + logger.debug(f"_on_site_info_failure - current site: {self._get_current_site()}") + + if self._get_current_site() != site_url: + logger.debug("_on_site_info_failure - site-info thread finished too late, we already selected another host") + return + + logger.debug("_on_site_info_failure - hiding spinner and showing error") + self.ui.refresh_site_info_spinner.setVisible(False) + self.ui.refresh_site_info_label.setVisible(False) + self.ui.message.setVisible(True) + self._set_error_message( + self.ui.message, f"Unable to communicate with FPTR site.
{exc}", + ) + + def _populate_user_dropdown(self, site: str) -> None: """ Populate the combo box of users based on a given site. :param str site: Site to populate the user list for. """ - if site: - users = session_cache.get_recent_users(site) - self.ui.login.set_recent_items(users) - else: - users = [] + + self.ui.login.clear() + + users = session_cache.get_recent_users(site) + self.ui.login.set_recent_items(users) if users: # The first user in the list is the most recent, so pick it. @@ -468,13 +557,29 @@ def _link_activated(self, site=None): self.ui.message, "Can't open '%s'." % forgot_password ) - def _toggle_web(self, method_selected=None): + def _update_login_page_ui(self, auth_method: int|None=None): """ Sets up the dialog GUI according to the use of web login or not. """ - site = self._query_task.url_to_test - self.method_selected_user = None + logger.info(f"on _update_login_page_ui auth_method: {auth_method}") + + site = self._get_current_site() + if self.site_info: + site = self.site_info.url + + if not self.site_info: + # thread is working at getting the info but we try to provide a best + # guess here. + prev_selected_method = session_cache.get_preferred_method(site) + else: + prev_selected_method = None + + + # what if url is empty or site_info is empty??? + + method_selected = auth_method + # self.method_selected_user = None ## WHY ????? # We only update the GUI if there was a change between to mode we # are showing and what was detected on the potential target site. @@ -484,7 +589,7 @@ def _toggle_web(self, method_selected=None): if can_use_web: # With a SSO site, we have no choice but to use the web to login. - can_use_web = self.site_info.sso_enabled + can_use_web = self.site_info.sso_enabled # TODO FIXME: in this situation, the legacy login/password method should not be available # The user may decide to force the use of the old dialog: # - due to graphical issues with Qt and its WebEngine @@ -504,6 +609,8 @@ def _toggle_web(self, method_selected=None): if method_selected: # Selecting requested mode (credentials, qt_web_login or app_session_launcher) self.method_selected_user = method_selected + # WHY ???????? TODO FIXME + # should be pass instead elif os.environ.get("SGTK_FORCE_STANDARD_LOGIN_DIALOG"): # Selecting legacy auth by default method_selected = auth_constants.METHOD_BASIC @@ -540,14 +647,13 @@ def _toggle_web(self, method_selected=None): else: method_selected = auth_constants.METHOD_BASIC - if site == self.host_selected and method_selected == self.method_selected: + if auth_method and method_selected == self.method_selected: # We don't want to go further if the UI is already configured for # this site and this mode. # This prevents erasing any error message when various events would # toggle this method return - self.host_selected = site self.method_selected = method_selected # if we are switching from one mode (using the web) to another (not using @@ -591,6 +697,7 @@ def _toggle_web(self, method_selected=None): self.ui.forgot_password_link.setVisible( method_selected == auth_constants.METHOD_BASIC + and self.site_info and self.site_info.user_authentication_method in ["default", "ldap"] ) @@ -609,13 +716,13 @@ def _toggle_web(self, method_selected=None): ) def _menu_activated_action_asl(self): - self._toggle_web(method_selected=auth_constants.METHOD_ASL) + self._update_login_page_ui(auth_method=auth_constants.METHOD_ASL) def _menu_activated_action_web_legacy(self): - self._toggle_web(method_selected=auth_constants.METHOD_WEB_LOGIN) + self._update_login_page_ui(auth_method=auth_constants.METHOD_WEB_LOGIN) def _menu_activated_action_login_creds(self): - self._toggle_web(method_selected=auth_constants.METHOD_BASIC) + self._update_login_page_ui(auth_method=auth_constants.METHOD_BASIC) def _current_page_changed(self, index): """ @@ -693,6 +800,19 @@ def result(self): return self._sso_saml2.get_session_data() + # # We want to wait until we know what is supported by the site, to avoid + # # flickering GUI. + # if not self._query_task.wait(THREAD_WAIT_TIMEOUT_MS): + # logger.warning( + # "Timed out awaiting requesting information: %s" + # % self._get_current_site() + # ) + + # Configure the GUI according to what we know: site and user preferences + # TODO -> self._update_login_page_ui.... + + self.ui.spinner_movie.start() + res = self.exec_() if res != QtGui.QDialog.Accepted: return @@ -749,7 +869,7 @@ def _ok_pressed(self): # Wait for any ongoing Site Configuration check thread. QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) try: - if not self._query_task.wait(THREAD_WAIT_TIMEOUT_MS): + if self._query_task and not self._query_task.wait(THREAD_WAIT_TIMEOUT_MS): logger.warning( "Timed out awaiting configuration information on the site: %s" % self._get_current_site() diff --git a/python/tank/authentication/resources/login_dialog.ui b/python/tank/authentication/resources/login_dialog.ui index b149820757..bb9af41d0f 100644 --- a/python/tank/authentication/resources/login_dialog.ui +++ b/python/tank/authentication/resources/login_dialog.ui @@ -148,7 +148,7 @@ QPushButton.main:pressed true - + 20 @@ -251,6 +251,31 @@ QPushButton.main:pressed + + + + + + + + Qt::AlignHCenter|Qt::AlignVCenter + + + + + + + Retrieving the site information... + + + Qt::AlignHCenter|Qt::AlignVCenter + + + + diff --git a/python/tank/authentication/resources/resources.qrc b/python/tank/authentication/resources/resources.qrc index 6e764ca76b..b67f9ba4d7 100644 --- a/python/tank/authentication/resources/resources.qrc +++ b/python/tank/authentication/resources/resources.qrc @@ -11,4 +11,7 @@ google_authenticator.png + + spinning_wheel.gif + diff --git a/python/tank/authentication/resources/spinning_wheel.gif b/python/tank/authentication/resources/spinning_wheel.gif new file mode 100644 index 0000000000..b74a22c4a8 Binary files /dev/null and b/python/tank/authentication/resources/spinning_wheel.gif differ diff --git a/python/tank/authentication/session_cache.py b/python/tank/authentication/session_cache.py index a96e8d40f2..2b2424a533 100644 --- a/python/tank/authentication/session_cache.py +++ b/python/tank/authentication/session_cache.py @@ -22,6 +22,7 @@ from __future__ import with_statement import os import socket +import time from tank_vendor.shotgun_api3 import ( Shotgun, AuthenticationFault, @@ -148,7 +149,33 @@ def _ensure_folder_for_file(filepath): return filepath +_YML_CONMTENT_CACHE = {} +_YML_CONMTENT_CACHE_TIMEOUT = 300 # 5 minutes + def _try_load_yaml_file(file_path): + global _YML_CONMTENT_CACHE + global _YML_CONMTENT_CACHE_TIMEOUT + + cache = _YML_CONMTENT_CACHE.get(file_path, None) + if cache and (time.time() - _YML_CONMTENT_CACHE_TIMEOUT > cache.get("timeout", 0)): + try: + del(_YML_CONMTENT_CACHE[file_path]) + except KeyError: + pass + + cache = None + + if not cache: + cache = { + "timeout": time.time(), + "content": _try_load_yaml_file_real(file_path), + } + _YML_CONMTENT_CACHE[file_path] = cache + + return cache["content"] + + +def _try_load_yaml_file_real(file_path): """ Loads a yaml file. @@ -306,6 +333,11 @@ def _write_yaml_file(file_path, users_data): finally: os.umask(old_umask) + global _YML_CONMTENT_CACHE + try: + del(_YML_CONMTENT_CACHE[file_path]) + except KeyError: + pass def delete_session_data(host, login): """ @@ -337,10 +369,10 @@ def delete_session_data(host, login): def get_session_data(base_url, login): """ - Returns the cached login info if found. + Returns the cached authentication info if found. - :param base_url: The site to look for the login information. - :param login: The user we want the login information for. + :param base_url: The site to look for the authentication information. + :param login: The user we want the authentication information for. :returns: Returns a dictionary with keys login and session_token or None """ @@ -368,10 +400,11 @@ def get_session_data(base_url, login): session_data[_SESSION_METADATA] = user[_SESSION_METADATA] return session_data - logger.debug("No cached user found for %s" % login) except Exception: logger.exception("Exception thrown while loading cached session info.") - return None + return + + logger.debug(f"No authentication info found for user {login}") def cache_session_data(host, login, session_token, session_metadata=None): @@ -539,7 +572,6 @@ def get_recent_users(site): """ info_path = _get_site_authentication_file_location(site) document = _try_load_site_authentication_file(info_path) - logger.debug("Recent users are: %s", document[_RECENT_USERS]) return _get_recent_items(document, _RECENT_USERS, _CURRENT_USER, "users") diff --git a/python/tank/authentication/shotgun_authenticator.py b/python/tank/authentication/shotgun_authenticator.py index 7987650d21..559be95207 100644 --- a/python/tank/authentication/shotgun_authenticator.py +++ b/python/tank/authentication/shotgun_authenticator.py @@ -232,6 +232,8 @@ def get_default_http_proxy(self): def get_default_user(self): """ Returns the default user from the defaults manager. + TODO this method's name and description is not accurate. This method tries to return the current user with credentials. ... + This is misleading...... :returns: A :class:`ShotgunUser` derived instance if available, None otherwise. """ @@ -240,8 +242,8 @@ def get_default_user(self): # There is no default user. if not credentials: - logger.debug("No default user found.") - return None + logger.debug("No default user found") + return # If this looks like an api user, delegate to create_script_user. # If some of the arguments are missing, don't worry, create_script_user diff --git a/python/tank/authentication/site_info.py b/python/tank/authentication/site_info.py index 1689c1b288..b96d35f149 100644 --- a/python/tank/authentication/site_info.py +++ b/python/tank/authentication/site_info.py @@ -19,118 +19,36 @@ logger = LogManager.get_logger(__name__) - -# Cache the servers infos for 30 seconds. -INFOS_CACHE_TIMEOUT = 30 -# This is a global state variable. It is used to cache information about the Shotgun servers we -# are interacting with. This is purely to avoid making multiple calls to the servers which would -# yield back the same information. (That info is relatively constant on a given server) -# Should this variable be cleared when doing a Python Core swap, it is not an issue. -# The side effect would be an additional call to the Shotgun site. -INFOS_CACHE = {} - - -def _get_site_infos(url, http_proxy=None): - """ - Get and cache the desired site infos. - - :param url: Url of the site to query. - :param http_proxy: HTTP proxy to use, if any. - - :returns: A dictionary with the site infos. - """ - - # Checks if the information is in the cache, is missing or out of date. - if url not in INFOS_CACHE or ( - (time.time() - INFOS_CACHE[url][0]) > INFOS_CACHE_TIMEOUT - ): - # Temporary shotgun instance, used only for the purpose of checking - # the site infos. - # - # The constructor of Shotgun requires either a username/login or - # key/scriptname pair or a session_token. The token is only used in - # calls which need to be authenticated. The 'info' call does not - # require authentication. - http_proxy = utils.sanitize_http_proxy(http_proxy).netloc - if http_proxy: - logger.debug("Using HTTP proxy to connect to the PTR server: %s", http_proxy) - - logger.info("Infos for site '%s' not in cache or expired", url) - sg = shotgun_api3.Shotgun( - url, session_token="dummy", connect=False, http_proxy=http_proxy - ) - # Remove delay between attempts at getting the site info. Since - # this is called in situations where blocking during multiple - # attempts can make UIs less responsive, we'll avoid sleeping. - # This change was introduced after delayed retries were added in - # python-api v3.0.41 - sg.config.rpc_attempt_interval = 0 - infos = sg.info() - - INFOS_CACHE[url] = (time.time(), infos) - else: - logger.info("Infos for site '%s' found in cache", url) - - return INFOS_CACHE[url][1] - - -class SiteInfo(object): - def __init__(self): - self._url = None - self._infos = {} - - def reload(self, url, http_proxy=None): +class SiteInfo: + def __init__(self, url: str, info: dict): """ - Load the site information into the instance. - - We want this method to fail as quickly as possible if there are any - issues. Failure is not considered critical, thus known exceptions are - silently ignored. At the moment this method used by the GUI show/hide - some of the input fields and by the console authentication to select the - appropriate authentication method. + TODO :param url: Url of the site to query. - :param http_proxy: HTTP proxy to use, if any. + :param info: site infor given by the API. """ - # Check for valid URL - url_items = utils.urlparse.urlparse(url) - if ( - not url_items.netloc - or url_items.netloc in "https" - or url_items.scheme not in ["http", "https"] - ): - logger.debug("Invalid Flow Production Tracking URL %s" % url) - return - - infos = {} - try: - infos = _get_site_infos(url, http_proxy) - # pylint: disable=broad-except - except Exception as exc: - # Silently ignore exceptions - logger.debug("Unable to connect with %s, got exception '%s'", url, exc) - return + logger.info("site_info start") self._url = url - self._infos = infos + self._infos = info - logger.debug("Site info for {url}".format(url=self._url)) + logger.debug(f"Site info for {self._url}") logger.debug( - " user_authentication_method: {value}".format( - value=self.user_authentication_method, - ) + f" user_authentication_method: {self.user_authentication_method}" ) logger.debug( - " unified_login_flow_enabled: {value}".format( - value=self.unified_login_flow_enabled, - ) + f" unified_login_flow_enabled: {self.unified_login_flow_enabled}" ) logger.debug( - " authentication_app_session_launcher_enabled: {value}".format( - value=self.app_session_launcher_enabled, - ) + f" authentication_app_session_launcher_enabled: {self.app_session_launcher_enabled}" ) + logger.info("site_info end") + + @property + def url(self): + return self._url + @property def user_authentication_method(self): """ @@ -188,3 +106,91 @@ def app_session_launcher_enabled(self): """ return self._infos.get("authentication_app_session_launcher_enabled", False) + +# Cache the servers infos for 30 seconds. +INFOS_CACHE_TIMEOUT = 30 +# This is a global state variable. It is used to cache information about the Shotgun servers we +# are interacting with. This is purely to avoid making multiple calls to the servers which would +# yield back the same information. (That info is relatively constant on a given server) +# Should this variable be cleared when doing a Python Core swap, it is not an issue. +# The side effect would be an additional call to the Shotgun site. +INFOS_CACHE = {} + + +def get(url: str, http_proxy: str|None=None, cache_only=False) -> SiteInfo|None: + """ + Get and cache the desired site infos. + + :param url: Url of the site to query. + :param http_proxy: HTTP proxy to use, if any. + # TODO cache_only and better desc + + :returns: A SiteInfo instance or None + """ + + # Checks if the information is in the cache, is missing or out of date. + if url not in INFOS_CACHE or ( + (time.time() - INFOS_CACHE[url][0]) > INFOS_CACHE_TIMEOUT + ): + if cache_only: + return + + si = _retrieve_site_info(url, http_proxy=http_proxy) + INFOS_CACHE[url] = (time.time(), si) + else: + logger.info(f'Infos for site "{url}" found in cache') + + return INFOS_CACHE[url][1] + +def _retrieve_site_info(url, http_proxy=None): + """ + Load the site information into the instance. + + We want this method to fail as quickly as possible if there are any + issues. Failure is not considered critical, thus known exceptions are + silently ignored. At the moment this method used by the GUI show/hide + some of the input fields and by the console authentication to select the + appropriate authentication method. + + """ + + # Check for valid URL + url_items = utils.urlparse.urlparse(url) + if ( + not url_items.netloc + or url_items.netloc in "https" + or url_items.scheme not in ["http", "https"] + ): + logger.debug("Invalid Flow Production Tracking URL %s" % url) + raise Exception("Invalid URL") + + # Temporary shotgun instance, used only for the purpose of checking + # the site infos. + # + # The constructor of Shotgun requires either a username/login or + # key/scriptname pair or a session_token. The token is only used in + # calls which need to be authenticated. The 'info' call does not + # require authentication. + http_proxy = utils.sanitize_http_proxy(http_proxy).netloc + if http_proxy: + logger.debug("Using HTTP proxy to connect to the PTR server: %s", http_proxy) + + logger.info("Infos for site '%s' not in cache or expired", url) + sg = shotgun_api3.Shotgun( + url, session_token="dummy", connect=False, http_proxy=http_proxy + ) + # Remove delay between attempts at getting the site info. Since + # this is called in situations where blocking during multiple + # attempts can make UIs less responsive, we'll avoid sleeping. + # This change was introduced after delayed retries were added in + # python-api v3.0.41 + sg.config.rpc_attempt_interval = 0 + + try: + infos = sg.info() + except Exception as exc: + # Silently ignore exceptions + logger.debug("Unable to connect with %s, got exception '%s'", url, exc) + return + + return SiteInfo(url, infos) diff --git a/python/tank/authentication/sso_saml2/core/sso_saml2_core.py b/python/tank/authentication/sso_saml2/core/sso_saml2_core.py index e148aa7d91..dfe524257e 100644 --- a/python/tank/authentication/sso_saml2/core/sso_saml2_core.py +++ b/python/tank/authentication/sso_saml2/core/sso_saml2_core.py @@ -222,17 +222,10 @@ def __init__(self, profile, parent, developer_mode=False): """ Class Constructor. """ - get_logger().debug("TKWebPageQtWebEngine.__init__") - super(TKWebPageQtWebEngine, self).__init__(profile, parent) + super().__init__(profile, parent) self._profile = profile self._developer_mode = developer_mode - def __del__(self): - """ - Class Destructor. - """ - get_logger().debug("TKWebPageQtWebEngine.__del__") - def acceptNavigationRequest(self, url, n_type, is_mainframe): """ Overloaded method, to properly control the behaviour of clicking on diff --git a/python/tank/authentication/ui/login_dialog.py b/python/tank/authentication/ui/login_dialog.py index 8cf2f9fcc8..21353bc6f1 100644 --- a/python/tank/authentication/ui/login_dialog.py +++ b/python/tank/authentication/ui/login_dialog.py @@ -153,9 +153,9 @@ def setupUi(self, LoginDialog): "}\n" "") LoginDialog.setModal(True) - self.verticalLayout_2 = QVBoxLayout(LoginDialog) - self.verticalLayout_2.setContentsMargins(20, 20, 20, 20) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.verticalLayout_asl = QVBoxLayout(LoginDialog) + self.verticalLayout_asl.setContentsMargins(20, 20, 20, 20) + self.verticalLayout_asl.setObjectName(u"verticalLayout_asl") self.horizontalLayout = QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName(u"horizontalLayout") @@ -174,7 +174,7 @@ def setupUi(self, LoginDialog): self.horizontalLayout.addWidget(self.logo) - self.verticalLayout_2.addLayout(self.horizontalLayout) + self.verticalLayout_asl.addLayout(self.horizontalLayout) self.stackedWidget = QStackedWidget(LoginDialog) self.stackedWidget.setObjectName(u"stackedWidget") @@ -207,6 +207,18 @@ def setupUi(self, LoginDialog): self.verticalLayout_7.addWidget(self.password) + self.refresh_site_info_spinner = QLabel(self.credentials) + self.refresh_site_info_spinner.setObjectName(u"refresh_site_info_spinner") + self.refresh_site_info_spinner.setAlignment(Qt.AlignHCenter|Qt.AlignVCenter) + + self.verticalLayout_7.addWidget(self.refresh_site_info_spinner) + + self.refresh_site_info_label = QLabel(self.credentials) + self.refresh_site_info_label.setObjectName(u"refresh_site_info_label") + self.refresh_site_info_label.setAlignment(Qt.AlignHCenter|Qt.AlignVCenter) + + self.verticalLayout_7.addWidget(self.refresh_site_info_label) + self.message = QLabel(self.credentials) self.message.setObjectName(u"message") sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) @@ -429,9 +441,9 @@ def setupUi(self, LoginDialog): self.stackedWidget.addWidget(self.backup_page) self.asl_page = QWidget() self.asl_page.setObjectName(u"asl_page") - self.verticalLayout_21 = QVBoxLayout(self.asl_page) - self.verticalLayout_21.setContentsMargins(20, 20, 20, 20) - self.verticalLayout_21.setObjectName(u"verticalLayout_21") + self.verticalLayout_2 = QVBoxLayout(self.asl_page) + self.verticalLayout_2.setContentsMargins(20, 20, 20, 20) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") self.asl_msg = QLabel(self.asl_page) self.asl_msg.setObjectName(u"asl_msg") sizePolicy1.setHeightForWidth(self.asl_msg.sizePolicy().hasHeightForWidth()) @@ -440,31 +452,31 @@ def setupUi(self, LoginDialog): self.asl_msg.setAlignment(Qt.AlignCenter) self.asl_msg.setWordWrap(True) - self.verticalLayout_21.addWidget(self.asl_msg) + self.verticalLayout_2.addWidget(self.asl_msg) self.asl_msg_back = QLabel(self.asl_page) self.asl_msg_back.setObjectName(u"asl_msg_back") self.asl_msg_back.setAlignment(Qt.AlignCenter) self.asl_msg_back.setWordWrap(True) - self.verticalLayout_21.addWidget(self.asl_msg_back) + self.verticalLayout_2.addWidget(self.asl_msg_back) self.asl_spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) - self.verticalLayout_21.addItem(self.asl_spacer) + self.verticalLayout_2.addItem(self.asl_spacer) self.asl_msg_help = QLabel(self.asl_page) self.asl_msg_help.setObjectName(u"asl_msg_help") self.asl_msg_help.setAlignment(Qt.AlignCenter) self.asl_msg_help.setWordWrap(True) - self.verticalLayout_21.addWidget(self.asl_msg_help) + self.verticalLayout_2.addWidget(self.asl_msg_help) self.stackedWidget.addWidget(self.asl_page) - self.verticalLayout_2.addWidget(self.stackedWidget) + self.verticalLayout_asl.addWidget(self.stackedWidget) - self.verticalLayout_2.setStretch(0, 1) + self.verticalLayout_asl.setStretch(0, 1) self.retranslateUi(LoginDialog) @@ -489,6 +501,8 @@ def retranslateUi(self, LoginDialog): self.password.setAccessibleName(QCoreApplication.translate("LoginDialog", u"password", None)) #endif // QT_CONFIG(accessibility) self.password.setPlaceholderText(QCoreApplication.translate("LoginDialog", u"password", None)) + self.refresh_site_info_spinner.setText("") + self.refresh_site_info_label.setText(QCoreApplication.translate("LoginDialog", u"Retrieving the site information...", None)) self.message.setText(QCoreApplication.translate("LoginDialog", u"Please enter your credentials.", None)) self.button_options.setText(QCoreApplication.translate("LoginDialog", u"See other options", None)) self.forgot_password_link.setText(QCoreApplication.translate("LoginDialog", u"

Forgot your password?

", None)) diff --git a/python/tank/authentication/ui/resources_rc.py b/python/tank/authentication/ui/resources_rc.py index 95c0853167..979110c25d 100644 --- a/python/tank/authentication/ui/resources_rc.py +++ b/python/tank/authentication/ui/resources_rc.py @@ -6,6 +6,770 @@ from .qt_abstraction import QtCore qt_resource_data = b"\ +\x00\x00/\x99\ +G\ +IF89a \x00 \x00p\x00\x00!\xf9\x04\x01\ +\x00\x00q\x00,\x00\x00\x00\x00 \x00 \x00\x86\x00\x00\ +\x00\x7f\x7f\x7f{{{zzz\x99\x99\x99\x8f\x8f\x8f\ +\x8d\x8d\x8d\x87\x87\x87yyyrrriiif\ +ff\x91\x91\x91\x8e\x8e\x8e|||hhhee\ +e\x97\x97\x97\x90\x90\x90wwwgggddd\ +ccc\x92\x92\x92\xb2\xb2\xb2\xa5\xa5\xa5\xa3\xa3\xa3\x96\ +\x96\x96\x8c\x8c\x8clllbbbUUUTT\ +T\xa6\xa6\xa6\xa4\xa5\xa4\xff\xff\xff\x95\x95\x95\x91\x90\x90\ +\x8b\x8b\x8b}}}mmm___WWWS\ +SS\xa4\xa4\xa4\xa2\xa2\xa2\xaa\xaa\xaa\x94\x94\x94XX\ +XRRRPPPZZZ\xa8\xa8\xa8VVV\ +\xb6\xb6\xb6QQQMMM333OOO\xb8\ +\xb8\xb8\xb7\xb7\xb7\xcc\xcc\xccCCC\xbb\xbb\xbb\xbc\xbc\ +\xbc\xbf\xbf\xbfBBB???@@@\xb4\xb4\xb4\ +\xc3\xc3\xc3$$$KKK\xca\xca\xca\xca\xc9\xcaH\ +HH555\xc8\xc8\xc8\xce\xce\xce\xd0\xd0\xd011\ +1222444\xd2\xc3\xd2\xcb\xcb\xcb\xcf\xcf\xcf\ +\xda\xda\xda\xde\xde\xde\xec\xec\xec\xf4\xf4\xf4---'\ +''+++000...\xcd\xcd\xcd\xdb\xdb\ +\xdb\xdd\xdd\xdd\xe0\xe0\xe0\xe2\xe2\xe2\xe9\xe9\xe9\xed\xed\xed\ +&&&)))***\xd4\xd4\xd4\xd6\xd6\xd6\xdf\ +\xdf\xdf\xe1\xe1\xe1\xea\xea\xea(((\xdc\xdc\xdc\xf0\xf0\ +\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xb9\ +\x80q\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\ +\x90\x87\x13\x13\x91\x8d\x93\x95\x87\x04\x07\x83\x97\x82\x0b\x0b\ +\x98q\x07\x04\x83\x07\x94\x9e\xa0\x98\xa3\x9bq\x9d\x9f\xa1\ +\x82\x9a\x82\xae\xa9\x8f\x04.\x84\xa3\xa2\x94\xaf\x83\x1f\x1f\ +\x8c..\xb7\xb1\xac\xa8\x82\xbf\x1f>\x8e\xc2\xc2\x88\xc9\ +\xcb\x91\xc4\x87\xca\xb0\xd6\x83?\xd9\xda?\x8a>\xde\xdf\ +\xd1\x86\xdb\xda\x8a9>\xe6\xe7\xe1\xd7\xa1==\x889\ +9\x95\xed\xed\xef\xf0\xf1\x8d\xf3\xeeqaa\x84GG\ +\x82\xec\xddSDo\x10\xbf8e\xca\xc4\xf97\xc8^\ +\xa4\x83\x08\x15\xfe\x03\x18\x8a_\xbf\x88\x82\x18V\xbc\x88\ +1 \xc5Ja\x14\x0aJ8H\xe35\x92\xeb\x0a\xa1\ +L\xc9\xb2\xa5\xcb\x97\x86\x02\x01\x00!\xf9\x04\x01\x00\x00\ +\x94\x00,\x00\x00\x00\x00 \x00 \x00\x87\x00\x00\x00\x94\ +\x94\x94\x92\x92\x92\x8f\x90\x90\x90\x90\x90\xbf\xbf\xbf\x96\x96\ +\x96\x91\x91\x91\x8e\x8e\x8e\x8f\x8f\x8f\xff\xff\xff\xa9\xa9\xa9\ +\xa5\xa5\xa5\xa3\xa5\xa5\x93\x93\x97\x8d\x8d\x8d}}}{\ +{{|||\xa8\xa8\xa8\xa6\xa6\xa6\xa3\xa3\xa3\x95\x95\ +\x95\x8d\x90\x8d\x84\x84\x84zzz\xa7\xa7\xa7\x99\x99\x99\ +\x8c\x8c\x8c~~~xxx\xa3\xa4\xa4\xa2\xa2\xa2\x82\ +\x82\x82yyy\xa2\xa3\xa3\x9d\x9d\x9d\x97\x97\x97\x8d\x8e\ +\x8e\x7f\x7f\x7f\xba\xba\xba\xb9\xb9\xb9\xaa\xaa\xaa\xa4\xa4\xa4\ +\xa1\xa2\xa2\x9a\x9a\x9a\x81\x81\x81qqqjjjk\ +kk\xbb\xbb\xbb\xb7\xb7\xb7\xb2\xb2\xb2\x89\x89\x89\x86\x86\ +\x86lllhhhgggfff\xbd\xbd\xbd\ +\xbc\xbc\xbc\xb8\xb8\xb8\xae\xae\xae\x93\x93\x93\x80\x80\x80w\ +wweee\xbb\xbc\xbc\xaa\xad\xad\x9f\x9f\x9fpp\ +piiighhddd\xb6\xb6\xb6\x5c\x5c\x5c\ +\xbe\xbf\xbfccc\xc0\xc0\xc0```\xcd\xcd\xcd\xcc\ +\xcc\xcc\xcb\xcb\xcbZZZWWWXXX\xcc\xcb\ +\xcc\xcb\xca\xcbVVVUUUTTT\xce\xce\xce\ +\xcf\xcf\xcfRRRQQQSSS\xd1\xd1\xd1\xd3\ +\xd3\xd3\xd4\xd4\xd4\xd6\xd6\xd6OOOPPP\xdd\xdd\ +\xdd\xdc\xdc\xdc\xdf\xdf\xdfNNNHHHEEE\ +\xdb\xdb\xdbAAACCCDDD\xde\xde\xde\xe0\ +\xe0\xe0\xe4\xe4\xe4>>>@@@BBB\xe2\xe2\ +\xe2\xe8\xe8\xe8\xec\xec\xec\xeb\xeb\xeb///444\ +777???\xe1\xe1\xe1\xe9\xe9\xe9\xee\xee\xee*\ +**+++33322255599\ +9\xed\xed\xed\xf1\xf1\xf1((()))---\ +111666\xea\xea\xea...888\xf4\ +\xf4\xf4,,,;;;\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\x00)\ +\x09\x1cH\xb0\xe0\x86\x1a\x05\x13*\x5cH\xb0\x06B\x86\ +\x10\x07\xaaPQ\xb0\xc6\x86\x82A\x82D$8\xf1\x22\ +A\x13\x0f\x07f\x0c\xb9QE\x03\x8a\x03-6\xcc\xb8\ +\x91\xe3\x06\x94\x02\x1d\x12\xcc\xa8\xb1\xa5\xc4\x93\x037x\ +\xa4Ds\xa3\x8c\x84*^\xc6|X\x83eA\x1d\x05\ +e(\xfd)q\xa2\xc0\x83\x02{\x0e\xd4A\x15\xe9\xc0\ +\xa52`R\x0a\x9a\xd0\xa8\xc0\xaaT\x13be\xba\xb0\ +&%\xb0V\x17b\xdd\x88\xd6\xa7\xd2\x88`m\xca\x9d\ +K\x97a\x94\xbbx\xf1\xda\xcc\xc2\xb7o\xdf\xbcw\xc7\ +\xdc\xddK\xc6o\xdf\xba\x88\x13\xdb4\xc3\xd8\x0c\xc4A\ +o\x22ol\xcc8b\xe4\xcb\x10);\x16\x08(\xe1\ +\xa0\x81\x91!\xbfI\xa8\x99\xa0\x99\xce\x05\x07}\x16x\ +\x19\xf3@\xca\x04\x01\xc9\x16\x98(\x91@\xd5\xabo\xbb\ +~\x9dP6\xea\xda\xb7U{\xde\xe8{ \xf0\xe0\xb9\ +m\x167n\x1byr\xe2\xa7\x09\x1eG.w9\xf3\ +\xd4\xc2\x95\xa3\xbeN0\xbb\xe2\xe9\x8a\x13\x82\x03\x8f\x18\ +\x10\x00!\xf9\x04\x01\x00\x00\x8e\x00,\x00\x00\x00\x00 \ +\x00 \x00\x87\x00\x00\x00\xb4\xb4\xb4\xa7\xa7\xa7\xa5\xa5\xa5\ +\xa3\xa3\xa3\xcc\xcc\xcc\xff\xff\xff\xa9\xa9\xa9\xa6\xa6\xa6\xa4\ +\xa4\xa4\xaa\xaa\xaa\xbc\xbc\xbc\xb8\xb8\xb8\xb9\xb9\xb9\xab\xab\ +\xab\x95\x95\x95\x92\x92\x92\x93\x93\x93\xbd\xbd\xbd\xba\xba\xba\ +\xb7\xb7\xb7\x96\x96\x96\x8f\x8f\x8f\x8e\x8e\x8e\xbb\xbb\xbb\xc6\ +\xc6\xc6\x90\x90\x90\x8d\x8d\x8d\x91\x91\x91\xc3\xc3\xc3\x98\x98\ +\x98\xcd\xcd\xcd\xc9\xc9\xc9\xbf\xbf\xbf\xb6\xb6\xb6\xa1\xa1\xa1\ +\x88\x88\x88\x7f\x7f\x7f\xcb\xcb\xcb\xbe\xbe\xbe\xac\xac\xac\xa8\ +\xa8\xa8\x9b\x9b\x9b\x8c\x8c\x8c\x82\x82\x82|||{{\ +{zzz\xcf\xcf\xcf\xc1\xc1\xc1\x94\x97\x94\x8b\x8b\x8b\ +~~~yyy\xd0\xd0\xd0\xce\xce\xce\xcb\xca\xcb\xc0\ +\xc0\xc0\x9f\x9f\x9f\x87\x87\x87xxx\xcc\xc9\xcc\x83\x83\ +\x83}}}\xd1\xd1\xd1\xce\xcf\xcfwww\xd3\xd3\xd3\ +\xd4\xd4\xd4uuu\xe0\xe0\xe0\xdb\xdb\xdb\xda\xda\xdan\ +nnlll\xdd\xdd\xdd\xdc\xdc\xdcjjjhh\ +hgggiii\xdf\xdf\xdf\xe1\xe1\xe1fff\ +dddeee\xe2\xe2\xe2\xe3\xe3\xe3\xe4\xe4\xe4\xe6\ +\xe6\xe6bbbaaa\xe9\xe9\xe9\xe8\xe8\xe8\xee\xee\ +\xeeYYY^^^\xeb\xeb\xeb\xed\xed\xedSSS\ +TTTVVVXXX]]]\xea\xea\xea\xec\ +\xec\xecPPPQQQZZZUUU\xef\xef\ +\xef???''')))EEEFFF\ +DDDHHHRRR(((---*\ +**333777@@@AAA__\ +_\xfe\xfe\xfe000555222===\ +BBB+++444$$$CCCJ\ +JJ,,,;;;666888\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x08\xff\x00\x1d\x09\x1cH\xb0\xa0\x02\x05\x05\ +\x13*\x5cH\xd0\x03B\x86\x10\x07b\xc0`\xf0\xe1@\ +\x0f$\x22\x12\x9cH\x91\xa0\x02\x0f\x051f\xd4(\x90\ +\xa3G\x90\x03I`$)q\xa2G\x8b\x8eD\xb2l\ +\xd9\xd1\xd1\xc1\x8b$FB,\x90\xd0\xa4M\x94\x18Q\ +\x12\x14R\xb0\x80Q\x9e4\x05\xde\x8c\x99\x93 \x09!\ +P\x09\x1e5\xba\xb1fJ\xa1\x8e\xa0jM8\x15\xe9\ +B\xa1Z\x9f\xee<\xaaQk\xd4\x88K\xa8B4;\ +\xb3\xad\xdb\xb7\x05\x97\xc8\x9d;\x97\xe5\x94\xbbx\xf1\xd2\ +\xddk7o^\xb8\x80\x03G\xf4B\xd8\x0b\xc46\x88\ +\xdbh$\xbc\x84\xb0\xc6\xc4\x8a\x196.,\xf0\xd0\xa1\ +\x84t\x06\xd2\x81\x9cp\xb2\xe3\x81\x96\x05\xea\xd1\x9cY\ +`\x9b\xcd\x89\x09\x16^B\xf0\x90\x9e\xcb\x8e\xf4\x8c\x8e\ +M\xa7\xb4\xe9\xd4\xaa\x0b\xbev-zvm\xdb\x03#\ +C\xb4\x0c;\xf6l\xda\xc0I\xee&(\x9bt\xed\xb6\ +\x96\x8f\x1b'\xf8{ft\xdd\xd2\x1dUg\x19\x9ay\ +\xf6\xe7\x82\x9b\x0b\x05V(\x9ed@\x00!\xf9\x04\x01\ +\x00\x00\x9e\x00,\x00\x00\x00\x00 \x00 \x00\x87\x00\x00\ +\x00\xb2\xb2\xb2\xbc\xbc\xbc\xba\xba\xba\xda\xda\xda\xff\xff\xff\ +\xc1\xbd\xc1\xbb\xbb\xbb\xb8\xb9\xb9\xb9\xb9\xb9\xaa\xaa\xaa\xd1\ +\xd1\xd1\xcd\xcd\xcd\xcc\xca\xcc\xc0\xc0\xc0\xa9\xa9\xa9\xa6\xa7\ +\xa6\xa8\xa8\xa8\xce\xce\xce\xcc\xcc\xcc\xc9\xc9\xc9\xb6\xb9\xb9\ +\xac\xac\xac\xa7\xa7\xa7\xa5\xa6\xa5\xa4\xa4\xa4\xcc\xcb\xcc\xc8\ +\xc8\xc8\xbf\xbf\xbf\xa7\xa9\xa7\xa6\xa6\xa6\xa3\xa3\xa3\xa5\xa5\ +\xa5\xcd\xcc\xcd\xca\xca\xca\xa9\xac\xa9\xcf\xcf\xcf\xcb\xcb\xcb\ +\xaf\xaf\xaf\xa2\xa2\xa2\xdd\xdd\xdd\xdc\xdc\xdc\xd2\xd2\xd2\xcb\ +\xc9\xcb\xc2\xc2\xc2\xb8\xb8\xb8\x9c\x9c\x9c\x94\x94\x94\xde\xde\ +\xde\xdb\xdb\xdb\xd0\xd0\xd0\xc3\xc3\xc3\xbd\xbc\xbd\xad\xb3\xb3\ +\xa8\xa9\xa8\xa2\xa3\xa2\x91\x91\x91\x96\x96\x96\x95\x95\x95\x92\ +\x93\x92\x8f\x8f\x8f\x7f\x7f\x7f\xe0\xe0\xe0\xdf\xdf\xdf\xd8\xd8\ +\xd8\xbe\xbe\xbe\x99\x99\x99\x97\x97\x97\x92\x92\x92\x8e\x8e\x8e\ +\xe1\xe1\xe1\xa4\xa7\xa4\xa0\xa0\xa0\x9b\x9b\x9b\x90\x90\x90\x8d\ +\x8d\x8d\x98\x98\x98\x93\x93\x93\x8b\x8b\x8b\x8c\x8c\x8c\xe7\xe7\ +\xe7\xe2\xe2\xe2\xe9\xe9\xe9\xea\xea\xea\xe8\xe8\xe8\x84\x84\x84\ +\x86\x86\x86\x82\x82\x82\x85\x85\x85\xeb\xeb\xeb\xec\xec\xec}\ +}}|||{{{\xed\xed\xed\xef\xef\xefyy\ +yxxx\xee\xee\xee\xf0\xf0\xf0zzz+++\ +(((333ssslllooo-\ +--&&&'''hhhiiiqq\ +q888)))ccecccfef\ +gggnnn,,,***UUU2\ +22444VVV[[[bbbdd\ +dfff555AAADDDQQQ\ +RRRZZZeeejjj1117\ +77===CCCFFFNNNPP\ +PSSSWWW???OOOeef\ +IIIMMM@@@TTT666B\ +BB>>>HHH\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\ +\x00=\x09\x1cH\xb0\xe0\x81\x03\x05\x13*\x5cH\xf0 \ +\xc3\x87\x04'L0\x88\x90\xa0\x02\x05\x10\x0bJ\xa4X\ +\xf0\xa2\x90\x8c\x03%N\x1c\xe8p\xe0E$ #n\ +\x14XR\xa0\x02!\x18S\x0a\x14I\xb2\xa2'\x8f\x19\ +Q$\xa4\xe9\xa9\xe4\xc9\x84\x1f\x09\xa2\x18\xaa3\xe4\xca\ +\x035\x5c\x22\x89)PH\x11!V\x84\x12\x1dj4\ +\xe1K\x82V\xa0fM8\xb5hF!`\xb72\x9c\ +\xfa\xd5\x8aY\x90d\x1f\x9a\x0d*\xb3\xad\xdb\xb7\x03\xc5\ +\xc8E1W\x0c]\x90a\xf2\xea\xb5\x92W\xae\xdf\xbf\ +b\xf0\xea\x1d\x1c\x06\xae\xe1\xc3\x10\xd9(f\xf30\x90\ +\xe3@\x19\x17+\xce\xf8\x182\xc3\xc5g\x18{:s\ +&\xa1\x9e\x81\x95-\x13\xc4\xacY g\x81\x84\x06\xea\ +\xf9\x0c\xba\xf2\xe8\xcc\xa57\x9f\xf6D(\xb5\xa7\xd5\xac\ +\x05\xba\x1e]\x90\xf3\xec\xda\xa8W'\x14\xcd\xd0\xf7@\ +\xe0\xc1s\xa74>\xf0\x8c\xed\xdb\xb8\xdb2G\xfd\x1c\ +\xbar\x88\xd3\xa9\x17\x8c\xbe\xbc3A\xe7\xdb\xaf\xbf\x08\ +E\x8e8!\xf9\x8c\x01\x01\x00!\xf9\x04\x01\x00\x00\xa8\ +\x00,\x00\x00\x00\x00 \x00 \x00\x87\x00\x00\x00\xd4\xd4\ +\xd4\xcf\xcf\xcf\xcd\xcd\xcd\xc6\xc6\xc6\xda\xda\xda\xce\xce\xce\ +\xcc\xcc\xcc\xff\xff\xff\xdd\xdd\xdd\xdc\xdc\xdc\x7f\x7f\x7f\xbe\ +\xbe\xbe\xbb\xbb\xbb\xba\xba\xba\xdf\xdf\xdf\xde\xde\xde\xdb\xdb\ +\xdb\xd2\xd2\xd2\xc0\xc0\xc0\xb7\xb9\xb9\xe1\xe1\xe1\xe9\xe9\xe9\ +\xbc\xbc\xbc\xb8\xb9\xb9\xb8\xb8\xb8\xe0\xe0\xe0\xb9\xb9\xb9\xe6\ +\xe6\xe6\xd8\xd8\xd8\xc1\xc1\xc1\xb7\xb7\xb7\xbf\xbf\xbf\xec\xec\ +\xec\xee\xee\xee\xe2\xe2\xe2\xca\xca\xca\xb1\xb1\xb1\xa8\xa9\xa9\ +\xa7\xa9\xa9\xeb\xeb\xeb\xe8\xe8\xe8\xe4\xe4\xe4\xd3\xd3\xd3\xcb\ +\xcb\xcc\xc2\xc2\xc2\x99\x99\x99\xb6\xb6\xb6\xab\xab\xab\xa7\xa7\ +\xa7\xa6\xa7\xa7\xa4\xa5\xa5\xed\xed\xed\xea\xea\xea\xe3\xe3\xe3\ +\xb7\xb8\xb8\xad\xad\xad\xa5\xa5\xa5\xa3\xa4\xa3\xa3\xa5\xa3\xe5\ +\xe5\xe5\xaf\xaf\xaf\xa9\xaa\xaa\xa5\xa6\xa6\xa4\xa4\xa4\xa3\xa4\ +\xa4\xf0\xf0\xf0\xe7\xe7\xe7\xa6\xa6\xa6\xa3\xa3\xa3\xa4\xa6\xa4\ +\xf2\xf2\xf2\xa8\xa8\xa8\xa2\xa2\xa2333...0\ +00---UUU\x9a\x9a\x9a\x99\x9c\x9c\x98\x9b\ +\x98\x97\x97\x97''',,,\x93\x96\x96\x92\x92\x92\ +\x91\x91\x91((()))\x90\x90\x90\x8d\x8d\x8d\x8e\ +\x8e\x8e\x8f\x8f\x8f777///+++\x8b\x8b\ +\x8b\x8c\x8c\x8c;;;366\x81\x81\x81\x83\x87\x87\ +888444222{{{|||}\ +~~\x87\x87\x87:::6664557:\ +:wwwyyy|}}\x80\x82\x82233\ +???CCCEEEsssjjjl\ +llxxxzzz{||~~~\x82\x82\ +\x82344555BBB@@@SSS\ +VVVaaadddhhhooo\x5c\ +\x5c\x5c===TTTAAADDDFF\ +FQQQTUUYYYbbbfff\ +qqq<<|\ +\xcc\xf8)0h\x02\x11+Q\xcd\xb4\xf8Q`R\x17\ +>\x5c$\x14\x0a5\xe5\xc3\xab>\x920\xe4:\xf4k\ +R\xa6\x0f\xb9B\xbc\x1a\xb3\xad\xdb\xb7\x05\xa7\xc8\x9d;\ +\x17e\x95*V\x5c\xb4\xd1\xab\x97\xae_\x94m\x02\x07\ +\xd6\xbb\x17\xae\xe1\xc3\x10\x95(V\xf2P\x8e\xe36\x19\ +\x17+\xce\xe8\xb82C\xc9\x8c\x05BJhi\xa0c\ +3\x8e\x13b&\xa8dsAK\x9d=W\x86>>\xa3\xa3\xa3\xa6\xa6\xa6\xa4\xa4\xa4===\ +;;;\xa5\xa5\xa5777444555\xa7\ +\xa7\xa7\xa6\xa7\xa6\xa7\xa8\xa7\xa8\xa8\xa822233\ +3\xb0\xb0\xb0\xa9\xa9\xa9\xac\xac\xac\xab\xab\xab\xab\xae\xab\ +\xb7\xb7\xb7\xba\xba\xba888\xb8\xb8\xb8\xb9\xb9\xb9\xbc\ +\xbc\xbc)))***///\xbf\xbf\xbf\xbb\xbb\ +\xbb\xbe\xc1\xc1+++'''(((\xba\xbb\xbb\ +\xbd\xbe\xbe\xc1\xc1\xc1\xcc\xcc\xcc\xcd\xcd\xcd\xec\xec\xec\xed\ +\xed\xed\xef\xef\xef111\xbb\xbc\xbc\xbd\xbd\xbd\xca\xca\ +\xca\xcb\xca\xcb\xce\xce\xce\xd2\xd2\xd2\xde\xde\xde\xe2\xe2\xe2\ +\xe9\xe9\xe9\xeb\xeb\xeb,,,\xb6\xb6\xb6\xd4\xd4\xd4\xc9\ +\xc9\xc9\xcf\xcf\xcf\xd3\xd3\xd3\xdc\xdc\xdc\xdf\xdf\xdf\xe6\xe6\ +\xe6\xea\xea\xea\xfe\xfe\xfe000\xbb\xbd\xbd\xbe\xbe\xbe\ +\xd0\xd0\xd0\xe7\xe7\xe7\xee\xee\xee---\xda\xda\xda\xdd\ +\xdd\xdd\xe8\xe8\xe8\xf4\xf4\xf4\xcb\xcb\xcb\xe1\xe1\xe1\xe4\xe4\ +\xe4\xe0\xe0\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x08\xff\x009\x09\x1cH\xb0\xe0\x80\x01\x05\x13*\x5cH\ +\xf0 \xc3\x87\x04\x13$0\x88\x90\xa0\x08\x11\x10\x09z\ +\x90H\xb1\xe0\xc5 \x19\x07J\x9c8\xd0\xe1\xc0\x8b\x18\ +C\x0a\xe4\xc0\xb1dE\x81\x1fU\x8al\xc9\xc9$'\ +\x94\x199$\x1c)\xd0$\xce\x82 \x09r\x18\xaas\ +&I\x9f\x17\x09\x06Y\x1aT \x12\x0eO\x8b\x0a\xa4\ +y\xb2)'\xa6Z\xac\xae|\xfat\xa0\xd4\x93\x03\xb1\ +j-8\xb4+D\xa6K3F=\x1b$\xab\xcc\xb7\ +p\xe3\x16\xfc\xf1\x03\x89\xdd\xbauUj\xd9\xcb\x97/\ +\x12\xba\x80\x01\xeb\xed\xdbW\xae\xe1\xc3\x10\xd5(\x86\xd8\ +\xa6\xcd\xde\x90\x8a\x173n\xdc\xe6\xa1\xe2A\x92\x17J\ +\x1aH\xb9q\xc2\xc8\x999\xc9\x91\x93P\xd2f\x81\x9d\ +=\x0f\x04Mp4iN\x95*\x09\xacd\x9a`\xe7\ +\x82j\x12\xba\x9e-\x9b\x93\xe9\xd3\xb63\xee\xe6=\x90\ +6p\x95\xc3\x89\x0b\xfc}\x5c\xb8\x9c\xde\xca\x97\xd7F\ +>\xba`l\xeb\xd3CV'x\x9d`v\xc3\xdd\x11\ +'\x04\x0c\x9f1 \x00;\ \x00\x00\x01\x8a\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -3675,6 +4439,11 @@ \x00s\ \x00h\x00o\x00t\x00g\x00u\x00n\x00_\x00a\x00u\x00t\x00h\x00e\x00n\x00t\x00i\x00c\ \x00a\x00t\x00i\x00o\x00n\ +\x00\x12\ +\x0d\xb9\x836\ +\x00s\ +\x00p\x00i\x00n\x00n\x00i\x00n\x00g\x00_\x00w\x00h\x00e\x00e\x00l\x00.\x00g\x00i\ +\x00f\ \x00\x0e\ \x04\xac<\xa7\ \x00d\ @@ -3697,22 +4466,24 @@ " qt_resource_struct = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x07\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x08\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00.\x00\x02\x00\x00\x00\x01\x00\x00\x00\x06\ +\x00\x00\x00.\x00\x02\x00\x00\x00\x01\x00\x00\x00\x07\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00L\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04\ +\x00\x00\x00L\x00\x02\x00\x00\x00\x02\x00\x00\x00\x05\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00~\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x8f05\xd1\xf4\ -\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x01\x00\x00\x01\x8e\ -\x00\x00\x01\x8f05\xd1\xf8\ -\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x01\x00\x00\xb99\ -\x00\x00\x01\x8f05\xd1\xf3\ -\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00\xdb\x22\ -\x00\x00\x01\x8f05\xd1\xf5\ +\x00\x00\x01\x97\x8f$\xb0j\ +\x00\x00\x00\xa8\x00\x00\x00\x00\x00\x01\x00\x00/\x9d\ +\x00\x00\x01\x97\x8f$\xb0N\ +\x00\x00\x00\xca\x00\x00\x00\x00\x00\x01\x00\x001+\ +\x00\x00\x01\x96\xcb\xb7\xc7v\ +\x00\x00\x01\x0a\x00\x00\x00\x00\x00\x01\x00\x00\xe8\xd6\ +\x00\x00\x01\x90,\xa0\x05b\ +\x00\x00\x01B\x00\x00\x00\x00\x00\x01\x00\x01\x0a\xbf\ +\x00\x00\x01\x90,\xa0\x05b\ " def qInitResources(): diff --git a/python/tank/log.py b/python/tank/log.py index a3c6486367..d8ce0630e9 100644 --- a/python/tank/log.py +++ b/python/tank/log.py @@ -425,6 +425,14 @@ def __new__(cls, *args, **kwargs): return cls.__instance + @classmethod + def reset_logger(cls): + if not cls.__instance: + return + + del cls.__instance + cls.__instance = None + @staticmethod def get_logger(log_name): """ @@ -677,7 +685,7 @@ def initialize_custom_handler(self, handler=None): handler = logging.StreamHandler() # example: [DEBUG tank.log] message message - formatter = logging.Formatter("[%(levelname)s %(name)s] %(message)s") + formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") handler.setFormatter(formatter)