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:
2016-06-26 17:42:21 -05:00
parent b2c34a1a7e
commit f6897e6401
2 changed files with 189 additions and 13 deletions

View File

@@ -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()
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

View File

@@ -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/"
@@ -34,24 +36,107 @@ class WM_OT_update_index(bpy.types.Operator):
"""Check for updated list of add-ons available for download"""
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"]
@@ -124,6 +209,25 @@ class WM_OT_addon_download_install(bpy.types.Operator):
bl_label = "Download and install selected add-on"
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:
@@ -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):
return {'FINISHED'}
self.loop.stop()
return {'CANCELLED'}
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 {'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