Async support: downloading, json parsing, and add-on install
Downloading, parsing index.json, and installing add-ons all now are handled asynchronously using the asyncio module. These operations will no longer block Blender, allowing them to run in the background.
This commit is contained in:
@@ -69,7 +69,18 @@ class PackageManagerPreferences(AddonPreferences):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("wm.update_index", text="Update List", icon='FILE_REFRESH')
|
||||
|
||||
split = layout.split(percentage=1.0/3)
|
||||
if (len(self.pm_addons) == 0 or networking.download_install_status in
|
||||
("Processing response", "Downloading update",
|
||||
"Processing failed", "Update failed")):
|
||||
split.label(text=networking.download_install_status or
|
||||
"Update add-on list.")
|
||||
else:
|
||||
split.label(text="Available add-ons:")
|
||||
split.separator()
|
||||
split.operator("wm.update_index", text="Update List", icon='FILE_REFRESH')
|
||||
|
||||
rows = 1 if len(self.pm_addons) == 0 else 4
|
||||
layout.template_list("UI_UL_list", "addons_list", self, "pm_addons",
|
||||
self, "pm_addons_index", rows=rows)
|
||||
@@ -93,7 +104,14 @@ class PackageManagerPreferences(AddonPreferences):
|
||||
else:
|
||||
split.label(text="Installed: No")
|
||||
split.separator()
|
||||
|
||||
if (networking.download_install_status not in
|
||||
("Processing response", "Downloading update",
|
||||
"Processing failed", "Update failed")):
|
||||
split.label(text=networking.download_install_status)
|
||||
else:
|
||||
split.separator()
|
||||
|
||||
split.operator("wm.addon_download_install",
|
||||
text="Install from Web",
|
||||
icon='URL').addon = addon.module_name
|
||||
|
@@ -21,10 +21,12 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import asyncio
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from bpy.props import StringProperty
|
||||
|
||||
download_install_status = ""
|
||||
log = logging.getLogger('networking')
|
||||
|
||||
INDEX_DOWNLOAD_URL = ("https://git.blender.org/gitweb/gitweb.cgi/"
|
||||
@@ -35,23 +37,106 @@ class WM_OT_update_index(bpy.types.Operator):
|
||||
bl_idname = "wm.update_index"
|
||||
bl_label = "Check for updated list of add-ons"
|
||||
|
||||
_timer = None
|
||||
_redraw = False
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
global download_install_status
|
||||
return (not download_install_status
|
||||
or download_install_status == "Install successful"
|
||||
or "failed" in download_install_status)
|
||||
|
||||
def __init__ (self):
|
||||
global download_install_status
|
||||
download_install_status = ""
|
||||
|
||||
self._redraw = False
|
||||
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.loop.stop()
|
||||
|
||||
def execute(self, context):
|
||||
# Update add-on index
|
||||
self.loop.stop()
|
||||
|
||||
self.status("Starting")
|
||||
|
||||
self.loop.run_in_executor(None, self.update_index)
|
||||
|
||||
wm = context.window_manager
|
||||
wm.modal_handler_add(self)
|
||||
self._timer = wm.event_timer_add(0.0001, context.window)
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def modal(self, context, event):
|
||||
global download_install_status
|
||||
|
||||
if self._redraw:
|
||||
context.area.tag_redraw()
|
||||
self._redraw = False
|
||||
|
||||
if "failed" in download_install_status:
|
||||
self.cancel(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if download_install_status == "":
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
def cancel(self, context):
|
||||
self.loop.stop()
|
||||
|
||||
wm = context.window_manager
|
||||
wm.event_timer_remove(self._timer)
|
||||
|
||||
def status(self, text):
|
||||
global download_install_status
|
||||
|
||||
download_install_status = text
|
||||
self._redraw = True
|
||||
|
||||
def update_index(self):
|
||||
self.status("Downloading update")
|
||||
index_file = self.download_index()
|
||||
|
||||
if index_file:
|
||||
self.status("Processing response")
|
||||
|
||||
if self.parse_json(index_file):
|
||||
self.status("")
|
||||
return
|
||||
|
||||
self.status("Processing failed")
|
||||
return
|
||||
|
||||
self.status("Update failed")
|
||||
|
||||
def download_index(self):
|
||||
# Download the index.json file
|
||||
try:
|
||||
req = urllib.request.urlopen(INDEX_DOWNLOAD_URL)
|
||||
index_file = req.read().decode('utf-8')
|
||||
req.close()
|
||||
except urllib.error.HTTPError as err:
|
||||
self.report({'ERROR'}, "Error requesting update: %s %s" % (str(err.code), err.reason))
|
||||
return {'CANCELLED'}
|
||||
log.warning("Error requesting update: %s %s", err.code, err.reason)
|
||||
return False
|
||||
except urllib.error.URLError as err:
|
||||
log.warning("Download failed with URLError: %s", err)
|
||||
return False
|
||||
|
||||
return index_file
|
||||
|
||||
def parse_json(self, index_file):
|
||||
# Parse downloaded file
|
||||
try:
|
||||
addon_list = json.loads(index_file)
|
||||
except ValueError as err:
|
||||
self.report({'ERROR'}, "Error: JSON file could not parse.")
|
||||
log.warning("ValueError: %s", err)
|
||||
return {'CANCELLED'}
|
||||
log.warning("JSON file could not parse. ValueError: %s", err)
|
||||
return False
|
||||
|
||||
# Get the add-on preferences
|
||||
prefs = bpy.context.user_preferences.addons.get("package_manager").preferences
|
||||
@@ -78,7 +163,7 @@ class WM_OT_update_index(bpy.types.Operator):
|
||||
|
||||
self.load_addon_data(addon, name, content)
|
||||
|
||||
return {'FINISHED'}
|
||||
return True
|
||||
|
||||
def load_addon_data(self, addon, module_name, content):
|
||||
addon.name = content["name"]
|
||||
@@ -125,6 +210,25 @@ class WM_OT_addon_download_install(bpy.types.Operator):
|
||||
|
||||
addon = bpy.props.StringProperty()
|
||||
|
||||
_timer = None
|
||||
_redraw = False
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
global download_install_status
|
||||
return (not download_install_status
|
||||
or download_install_status == "Install successful"
|
||||
or "failed" in download_install_status)
|
||||
|
||||
def __init__ (self):
|
||||
global download_install_status
|
||||
download_install_status = ""
|
||||
|
||||
self._redraw = False
|
||||
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.loop.stop()
|
||||
|
||||
def execute(self, context):
|
||||
if self.addon is None:
|
||||
return {'CANCELLED'}
|
||||
@@ -144,11 +248,61 @@ class WM_OT_addon_download_install(bpy.types.Operator):
|
||||
ext = os.path.splitext(download_url)[1]
|
||||
|
||||
# Download and install the selected add-on
|
||||
if self.download(self.addon, download_url):
|
||||
if self.install(self.addon, ext):
|
||||
self.loop.stop()
|
||||
|
||||
self.status("Starting")
|
||||
|
||||
self.loop.run_in_executor(None, self.download_and_install,
|
||||
self.addon, download_url, ext)
|
||||
|
||||
wm = context.window_manager
|
||||
wm.modal_handler_add(self)
|
||||
self._timer = wm.event_timer_add(0.0001, context.window)
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def modal(self, context, event):
|
||||
global download_install_status
|
||||
|
||||
if self._redraw:
|
||||
context.area.tag_redraw()
|
||||
self._redraw = False
|
||||
|
||||
if "failed" in download_install_status:
|
||||
self.cancel(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if download_install_status == "Install successful":
|
||||
self.cancel(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'CANCELLED'}
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
def cancel(self, context):
|
||||
self.loop.stop()
|
||||
|
||||
wm = context.window_manager
|
||||
wm.event_timer_remove(self._timer)
|
||||
|
||||
def status(self, text):
|
||||
global download_install_status
|
||||
|
||||
download_install_status = text
|
||||
self._redraw = True
|
||||
|
||||
def download_and_install (self, addon, download_url, filetype):
|
||||
# Perform download; if successful, install
|
||||
self.status("Downloading")
|
||||
if self.download(addon, download_url):
|
||||
self.status("Installing")
|
||||
if self.install(addon, filetype):
|
||||
self.status("Install successful")
|
||||
return
|
||||
self.status("Install failed")
|
||||
return
|
||||
|
||||
self.status("Download failed")
|
||||
self._redraw = True
|
||||
|
||||
def download(self, addon, download_url):
|
||||
filetype = os.path.splitext(download_url)[1]
|
||||
@@ -165,6 +319,9 @@ class WM_OT_addon_download_install(bpy.types.Operator):
|
||||
except urllib.error.HTTPError as err:
|
||||
log.warning("Download failed with HTTPError: %s %s", str(err.code), err.reason)
|
||||
return False
|
||||
except urllib.error.URLError as err:
|
||||
log.warning("Download failed with URLError: %s", err)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -184,7 +341,8 @@ class WM_OT_addon_download_install(bpy.types.Operator):
|
||||
shutil.rmtree(addon_path)
|
||||
with zipfile.ZipFile(download_path,"r") as zipped_addon:
|
||||
zipped_addon.extractall(bpy.utils.user_resource('SCRIPTS', path="addons"))
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
Reference in New Issue
Block a user