450 lines
17 KiB
Python
450 lines
17 KiB
Python
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
import logging
|
|
import os
|
|
import queue
|
|
import requests
|
|
|
|
import bpy
|
|
|
|
from . import (
|
|
addon_updater_ops,
|
|
asset_bar_op,
|
|
bg_blender,
|
|
bkit_oauth,
|
|
categories,
|
|
client_lib,
|
|
client_tasks,
|
|
comments_utils,
|
|
disclaimer_op,
|
|
download,
|
|
global_vars,
|
|
persistent_preferences,
|
|
ratings_utils,
|
|
reports,
|
|
search,
|
|
tasks_queue,
|
|
upload,
|
|
utils,
|
|
)
|
|
|
|
|
|
bk_logger = logging.getLogger(__name__)
|
|
reports_queue: queue.Queue = queue.Queue()
|
|
pending_tasks = (
|
|
list()
|
|
) # pending tasks are tasks that were not parsed correclty and should be tried to be parsed later.
|
|
|
|
|
|
def handle_failed_reports(exception: Exception) -> float:
|
|
"""Function reacting to failing reports (Client is not accessible).
|
|
On 11th, 21st, 31st etc. it will print error message and start Client on other ports.
|
|
Iterating over the available ports for each start. Users did not want to change the
|
|
ports manually, so we do this automatically for them.
|
|
"""
|
|
global_vars.CLIENT_ACCESSIBLE = False
|
|
global_vars.CLIENT_FAILED_REPORTS += 1 # De facto means we count from 1, not from 0
|
|
|
|
# First failed report -> lets start the Client
|
|
if global_vars.CLIENT_FAILED_REPORTS == 1:
|
|
### Expected - port probably free as connection was refused
|
|
if isinstance(exception, requests.ConnectionError):
|
|
bk_logger.info(
|
|
f"Expectedly, first request for BKClient reports failed: {str(exception).strip()} {type(exception)}"
|
|
)
|
|
### Something unsupported runs on the port (other program, or Client refusing for version reasons)
|
|
elif isinstance(exception, requests.HTTPError):
|
|
bk_logger.info(
|
|
f"First request for BKClient reports was rejected: {str(exception).strip()} {type(exception)}. Port is occupied and has to be changed"
|
|
)
|
|
client_lib.reorder_ports()
|
|
# Not so expected
|
|
else:
|
|
bk_logger.warning(
|
|
f"First request for BKClient reports failed unexpectedly: {str(exception).strip()} {type(exception)}"
|
|
)
|
|
client_lib.start_blenderkit_client()
|
|
else:
|
|
bk_logger.warning(
|
|
f"Request for BKClient reports failed: {str(exception).strip()} {type(exception)}"
|
|
)
|
|
|
|
if global_vars.CLIENT_FAILED_REPORTS <= 10: # try 10 times
|
|
return 0.1 * global_vars.CLIENT_FAILED_REPORTS
|
|
|
|
# MORE THAN 10 FAILURES - enough time for the Client to get up and running
|
|
# so we need to investigate why it failed to start and respond correctly
|
|
log_msg = f"Could not get reports ({global_vars.CLIENT_FAILED_REPORTS}. failure): {str(exception).strip()} {type(exception)}"
|
|
return_code, meaning = client_lib.check_blenderkit_client_return_code()
|
|
|
|
# On FAILED_REPORTS == 11, 21, 31...
|
|
if global_vars.CLIENT_FAILED_REPORTS % 10 == 1:
|
|
reports.add_report(log_msg, 5, "ERROR") # Let's show the message to user
|
|
if return_code == -1:
|
|
msg = "Client is not responding, add-on will not work."
|
|
reports.add_report(msg, timeout=10, type="ERROR")
|
|
if return_code != -1:
|
|
msg = f"Client failed to start, add-on will not work. Error({return_code}): {meaning}"
|
|
reports.add_report(msg, timeout=10, type="ERROR")
|
|
# LETS START AGAIN - on different port
|
|
# The catch is that the error message printed to user is outdated now.
|
|
# But there is not a better solution.
|
|
client_lib.reorder_ports()
|
|
client_lib.start_blenderkit_client()
|
|
else: # On FAILED_REPORTS == 12..20,22..30,32..40 we just log into terminal
|
|
bk_logger.warning(log_msg)
|
|
|
|
wm = bpy.context.window_manager
|
|
wm.blenderkitUI.logo_status = "logo_offline" # type: ignore[attr-defined]
|
|
global_vars.CLIENT_RUNNING = False
|
|
|
|
# Gradually retry less frequently, but at least once in 30s...
|
|
return min(30.0, 0.1 * global_vars.CLIENT_FAILED_REPORTS)
|
|
|
|
|
|
@bpy.app.handlers.persistent
|
|
def client_communication_timer():
|
|
"""Receive all responses from Client and run according followup commands.
|
|
This function is the only one responsible for keeping the Client up and running.
|
|
"""
|
|
global pending_tasks
|
|
bk_logger.log(5, "Getting tasks from Client")
|
|
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
|
if user_preferences.use_clipboard_scan:
|
|
search.check_clipboard()
|
|
results = list()
|
|
try:
|
|
results = client_lib.get_reports(os.getpid())
|
|
global_vars.CLIENT_FAILED_REPORTS = 0
|
|
except Exception as e:
|
|
return handle_failed_reports(e)
|
|
|
|
if global_vars.CLIENT_ACCESSIBLE is False:
|
|
bk_logger.info(
|
|
f"BlenderKit-Client is running on port {global_vars.CLIENT_PORTS[0]}!"
|
|
)
|
|
global_vars.CLIENT_ACCESSIBLE = True
|
|
wm = bpy.context.window_manager
|
|
wm.blenderkitUI.logo_status = "logo"
|
|
|
|
bk_logger.log(5, "Handling tasks")
|
|
results_converted_tasks = []
|
|
|
|
# convert to task type
|
|
for task in results:
|
|
task = client_tasks.Task(
|
|
data=task["data"],
|
|
task_id=task["task_id"],
|
|
app_id=task["app_id"],
|
|
task_type=task["task_type"],
|
|
message=task["message"],
|
|
message_detailed=task["message_detailed"],
|
|
progress=task["progress"],
|
|
status=task["status"],
|
|
result=task["result"],
|
|
)
|
|
results_converted_tasks.append(task)
|
|
|
|
# add pending tasks which were already parsed but not handled
|
|
results_converted_tasks.extend(pending_tasks)
|
|
pending_tasks.clear()
|
|
|
|
for task in results_converted_tasks:
|
|
handle_task(task)
|
|
|
|
bk_logger.log(5, "Task handling finished")
|
|
delay = user_preferences.client_polling
|
|
if len(download.download_tasks) > 0:
|
|
return min(0.2, delay)
|
|
return delay
|
|
|
|
|
|
@bpy.app.handlers.persistent
|
|
def timer_image_cleanup():
|
|
imgs = bpy.data.images[:]
|
|
for i in imgs:
|
|
if (
|
|
(i.name[:11] == ".thumbnail_" or i.filepath.find("bkit_g") > -1)
|
|
and not i.has_data
|
|
and i.users == 0
|
|
):
|
|
bpy.data.images.remove(i)
|
|
return 60
|
|
|
|
|
|
def save_prefs_cancel_all_tasks_and_restart_client(user_preferences, context):
|
|
"""Save preferences, cancel all blenderkit-client tasks, shutdown the blenderkit-client and reorder ports.
|
|
Unset the CLIENT_FAILED_REPORTS and restart client_communication_timer() so add-on will check for the reports ASAP.
|
|
Timer func client_communication_timer() will take care of starting the Client and checking the reports.
|
|
"""
|
|
utils.save_prefs(user_preferences, context)
|
|
if user_preferences.preferences_lock == True:
|
|
return
|
|
|
|
reports.add_report("Restarting Client server", timeout=2)
|
|
try:
|
|
cancel_all_tasks(user_preferences, context)
|
|
client_lib.shutdown_client()
|
|
except Exception as e:
|
|
bk_logger.warning(str(e))
|
|
|
|
client_lib.reorder_ports(
|
|
user_preferences.client_port
|
|
) # reorder after shutdown was requested
|
|
global_vars.CLIENT_FAILED_REPORTS = 0 # reset failed reports so next attempt to get report or start client is immediate
|
|
bpy.app.timers.unregister(client_communication_timer)
|
|
bpy.app.timers.register(client_communication_timer, persistent=True)
|
|
|
|
|
|
def trusted_CA_certs_property_updated(user_preferences, context):
|
|
"""Update trusted CA certs environment variables and call save_prefs()."""
|
|
update_trusted_CA_certs(user_preferences.trusted_ca_certs)
|
|
return save_prefs_cancel_all_tasks_and_restart_client(user_preferences, context)
|
|
|
|
|
|
def update_trusted_CA_certs(certs: str):
|
|
if certs == "":
|
|
os.environ.pop("REQUESTS_CA_BUNDLE", None)
|
|
os.environ.pop("CURL_CA_BUNDLE", None)
|
|
return
|
|
|
|
os.environ["REQUESTS_CA_BUNDLE"] = certs
|
|
os.environ["CURL_CA_BUNDLE"] = certs
|
|
return
|
|
|
|
|
|
def cancel_all_tasks(self, context):
|
|
"""Cancel all tasks."""
|
|
global pending_tasks
|
|
pending_tasks.clear()
|
|
download.clear_downloads()
|
|
search.clear_searches()
|
|
# TODO: should add uploads
|
|
|
|
|
|
def task_error_overdrive(task: client_tasks.Task) -> None:
|
|
"""Handle error task - overdrive some error messages, trigger functions common for all errors."""
|
|
if task.message.count("Invalid token.") > 0 and utils.user_logged_in():
|
|
preferences = bpy.context.preferences.addons[__package__].preferences # type: ignore
|
|
|
|
# Invalid token and api_key_refresh present -> trying to refresh the token
|
|
if preferences.api_key_refresh != "": # type: ignore
|
|
client_lib.refresh_token(preferences.api_key_refresh, preferences.api_key) # type: ignore
|
|
msg = "Invalid API key token. Refreshing the token now. If problem persist, please log-out and log-in."
|
|
reports.add_report(msg, type="ERROR")
|
|
return
|
|
|
|
# Invalid token and no api_key_refresh token -> nothing else we can try...
|
|
bkit_oauth.logout()
|
|
msg = "Invalid permanent API key token. Logged out. Please login again."
|
|
reports.add_report(msg, type="ERROR")
|
|
|
|
|
|
def handle_task(task: client_tasks.Task):
|
|
"""Handle incomming task information. Sort tasks by type and call apropriate functions."""
|
|
if task.status == "error":
|
|
task_error_overdrive(task)
|
|
|
|
# HANDLE ASSET DOWNLOAD
|
|
if task.task_type == "asset_download":
|
|
return download.handle_download_task(task)
|
|
|
|
# HANDLE ASSET UPLOAD
|
|
if task.task_type == "asset_upload":
|
|
return upload.handle_asset_upload(task)
|
|
|
|
if task.task_type == "asset_metadata_upload":
|
|
return upload.handle_asset_metadata_upload(task)
|
|
|
|
# HANDLE SEARCH (candidate to be a function)
|
|
if task.task_type == "search":
|
|
if task.status == "finished":
|
|
return search.handle_search_task(task)
|
|
elif task.status == "error":
|
|
return search.handle_search_task_error(task)
|
|
|
|
# HANDLE THUMBNAIL DOWNLOAD (candidate to be a function)
|
|
if task.task_type == "thumbnail_download":
|
|
return search.handle_thumbnail_download_task(task)
|
|
|
|
# HANDLE LOGIN
|
|
if task.task_type == "login":
|
|
return bkit_oauth.handle_login_task(task)
|
|
|
|
# HANDLE TOKEN REFRESH - most likely not needed anymore, TODO: remove
|
|
if task.task_type == "token_refresh":
|
|
return bkit_oauth.handle_token_refresh_task(task)
|
|
|
|
# HANDLE OAUTH LOGOUT
|
|
if task.task_type == "oauth2/logout":
|
|
return bkit_oauth.handle_logout_task(task)
|
|
|
|
# HANDLE CLIENT STATUS REPORT
|
|
if task.task_type == "client_status":
|
|
return client_lib.handle_client_status_task(task)
|
|
|
|
# HANDLE DISCLAIMER
|
|
if task.task_type == "disclaimer":
|
|
return disclaimer_op.handle_disclaimer_task(task)
|
|
|
|
# HANDLE CATEGORIES FETCH
|
|
if task.task_type == "categories_update":
|
|
return categories.handle_categories_task(task)
|
|
|
|
# HANDLE NOTIFICATIONS FETCH
|
|
if task.task_type == "notifications":
|
|
return comments_utils.handle_notifications_task(task)
|
|
|
|
# HANDLE VARIOUS COMMENTS TASKS
|
|
if task.task_type == "comments/get_comments":
|
|
return comments_utils.handle_get_comments_task(task)
|
|
if task.task_type == "comments/create_comment":
|
|
return comments_utils.handle_create_comment_task(task)
|
|
if task.task_type == "comments/feedback_comment":
|
|
return comments_utils.handle_feedback_comment_task(task)
|
|
if task.task_type == "comments/mark_comment_private":
|
|
return comments_utils.handle_mark_comment_private_task(task)
|
|
|
|
# HANDLE PROFILE
|
|
if task.task_type == "profiles/fetch_gravatar_image":
|
|
return search.handle_fetch_gravatar_task(task)
|
|
if task.task_type == "profiles/get_user_profile":
|
|
return search.handle_get_user_profile(task)
|
|
|
|
# HANDLE RATINGS
|
|
if task.task_type == "ratings/get_rating":
|
|
return ratings_utils.handle_get_rating_task(task)
|
|
if task.task_type == "ratings/get_ratings":
|
|
return ratings_utils.handle_get_ratings_task(task)
|
|
if task.task_type == "ratings/send_rating":
|
|
return ratings_utils.handle_send_rating_task(task)
|
|
|
|
# HANDLE BOOKMARKS
|
|
if task.task_type == "ratings/get_bookmarks":
|
|
return ratings_utils.handle_get_bookmarks_task(task)
|
|
|
|
# HANDLE NONBLOCKING_REQUEST
|
|
if task.task_type == "wrappers/nonblocking_request":
|
|
return utils.handle_nonblocking_request_task(task)
|
|
|
|
# BKCLIENTJS - Download from web
|
|
if task.task_type == "bkclientjs/get_asset":
|
|
return asset_bar_op.handle_bkclientjs_get_asset(task)
|
|
|
|
# HANDLE MESSAGE FROM CLIENT
|
|
if (
|
|
task.task_type == "message_from_daemon" # TODO: depracate message_from_daemon
|
|
or task.task_type == "message_from_client"
|
|
):
|
|
level = task.result.get("level", "INFO").upper()
|
|
duration = task.result.get("duration", 5)
|
|
destination = task.result.get("destination", "GUI")
|
|
if destination == "GUI":
|
|
return reports.add_report(task.message, duration, level)
|
|
if level == "INFO" or level == "VALIDATOR":
|
|
return bk_logger.info(task.message)
|
|
if level == "WARNING":
|
|
return bk_logger.warning(task.message)
|
|
if level == "ERROR":
|
|
return bk_logger.error(task.message)
|
|
|
|
|
|
@bpy.app.handlers.persistent
|
|
def check_timers_timer():
|
|
"""Checks if all timers are registered regularly. Prevents possible bugs from stopping the addon."""
|
|
if not bpy.app.timers.is_registered(tasks_queue.queue_worker):
|
|
bpy.app.timers.register(tasks_queue.queue_worker)
|
|
if not bpy.app.timers.is_registered(bg_blender.bg_update):
|
|
bpy.app.timers.register(bg_blender.bg_update)
|
|
if not bpy.app.timers.is_registered(client_communication_timer):
|
|
bpy.app.timers.register(client_communication_timer, persistent=True)
|
|
if not bpy.app.timers.is_registered(timer_image_cleanup):
|
|
bpy.app.timers.register(timer_image_cleanup, persistent=True, first_interval=60)
|
|
return 5.0
|
|
|
|
|
|
def on_startup_timer():
|
|
"""Run once on the startup of add-on (Blender start with enabled add-on, add-on enabled)."""
|
|
persistent_preferences.load_preferences_from_JSON()
|
|
addon_updater_ops.check_for_update_background()
|
|
utils.check_globaldir_permissions()
|
|
|
|
return None
|
|
|
|
|
|
def on_startup_client_online_timer():
|
|
"""Run once when Client is online after startup."""
|
|
if not global_vars.CLIENT_RUNNING:
|
|
return 1
|
|
|
|
preferences = bpy.context.preferences.addons[__package__].preferences
|
|
refresh_needed = bkit_oauth.ensure_token_refresh()
|
|
if refresh_needed: # called for new API token, lets wait for a while
|
|
return 1
|
|
|
|
if preferences.show_on_start:
|
|
search.search()
|
|
|
|
return
|
|
|
|
|
|
def register_timers():
|
|
"""Register all timers if add-on is not running in background (thumbnail rendering, upload, unpacking and also tests).
|
|
It registers check_timers_timer which registers all other periodic non-ending timers.
|
|
And individually it register all timers which are expected to end.
|
|
"""
|
|
if bpy.app.background:
|
|
return
|
|
|
|
# PERIODIC TIMERS
|
|
bpy.app.timers.register(
|
|
check_timers_timer, persistent=True
|
|
) # registers all other non-ending timers
|
|
|
|
# ONETIMERS
|
|
bpy.app.timers.register(on_startup_timer)
|
|
bpy.app.timers.register(on_startup_client_online_timer, first_interval=1)
|
|
bpy.app.timers.register(disclaimer_op.show_disclaimer_timer, first_interval=1)
|
|
|
|
|
|
def unregister_timers():
|
|
"""Unregister all timers at the very start of unregistration.
|
|
This prevents the timers being called before the unregistration finishes.
|
|
"""
|
|
if bpy.app.background:
|
|
return
|
|
|
|
if bpy.app.timers.is_registered(check_timers_timer):
|
|
bpy.app.timers.unregister(check_timers_timer)
|
|
if bpy.app.timers.is_registered(tasks_queue.queue_worker):
|
|
bpy.app.timers.unregister(tasks_queue.queue_worker)
|
|
if bpy.app.timers.is_registered(bg_blender.bg_update):
|
|
bpy.app.timers.unregister(bg_blender.bg_update)
|
|
if bpy.app.timers.is_registered(client_communication_timer):
|
|
bpy.app.timers.unregister(client_communication_timer)
|
|
if bpy.app.timers.is_registered(timer_image_cleanup):
|
|
bpy.app.timers.unregister(timer_image_cleanup)
|
|
|
|
if bpy.app.timers.is_registered(on_startup_timer):
|
|
bpy.app.timers.unregister(on_startup_timer)
|
|
if bpy.app.timers.is_registered(on_startup_client_online_timer):
|
|
bpy.app.timers.unregister(on_startup_client_online_timer)
|
|
if bpy.app.timers.is_registered(disclaimer_op.show_disclaimer_timer):
|
|
bpy.app.timers.unregister(disclaimer_op.show_disclaimer_timer)
|