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): def draw(self, context):
layout = self.layout 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 rows = 1 if len(self.pm_addons) == 0 else 4
layout.template_list("UI_UL_list", "addons_list", self, "pm_addons", layout.template_list("UI_UL_list", "addons_list", self, "pm_addons",
self, "pm_addons_index", rows=rows) self, "pm_addons_index", rows=rows)
@@ -93,7 +104,14 @@ class PackageManagerPreferences(AddonPreferences):
else: else:
split.label(text="Installed: No") split.label(text="Installed: No")
split.separator() 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", split.operator("wm.addon_download_install",
text="Install from Web", text="Install from Web",
icon='URL').addon = addon.module_name icon='URL').addon = addon.module_name

View File

@@ -21,10 +21,12 @@ import json
import logging import logging
import os import os
import shutil import shutil
import asyncio
import urllib.request import urllib.request
import zipfile import zipfile
from bpy.props import StringProperty from bpy.props import StringProperty
download_install_status = ""
log = logging.getLogger('networking') log = logging.getLogger('networking')
INDEX_DOWNLOAD_URL = ("https://git.blender.org/gitweb/gitweb.cgi/" 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_idname = "wm.update_index"
bl_label = "Check for updated list of add-ons" 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): 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 # Download the index.json file
try: try:
req = urllib.request.urlopen(INDEX_DOWNLOAD_URL) req = urllib.request.urlopen(INDEX_DOWNLOAD_URL)
index_file = req.read().decode('utf-8') index_file = req.read().decode('utf-8')
req.close() req.close()
except urllib.error.HTTPError as err: except urllib.error.HTTPError as err:
self.report({'ERROR'}, "Error requesting update: %s %s" % (str(err.code), err.reason)) log.warning("Error requesting update: %s %s", err.code, err.reason)
return {'CANCELLED'} 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 # Parse downloaded file
try: try:
addon_list = json.loads(index_file) addon_list = json.loads(index_file)
except ValueError as err: except ValueError as err:
self.report({'ERROR'}, "Error: JSON file could not parse.") log.warning("JSON file could not parse. ValueError: %s", err)
log.warning("ValueError: %s", err) return False
return {'CANCELLED'}
# Get the add-on preferences # Get the add-on preferences
prefs = bpy.context.user_preferences.addons.get("package_manager").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) self.load_addon_data(addon, name, content)
return {'FINISHED'} return True
def load_addon_data(self, addon, module_name, content): def load_addon_data(self, addon, module_name, content):
addon.name = content["name"] addon.name = content["name"]
@@ -125,6 +210,25 @@ class WM_OT_addon_download_install(bpy.types.Operator):
addon = bpy.props.StringProperty() 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): def execute(self, context):
if self.addon is None: if self.addon is None:
return {'CANCELLED'} return {'CANCELLED'}
@@ -144,11 +248,61 @@ class WM_OT_addon_download_install(bpy.types.Operator):
ext = os.path.splitext(download_url)[1] ext = os.path.splitext(download_url)[1]
# Download and install the selected add-on # Download and install the selected add-on
if self.download(self.addon, download_url): self.loop.stop()
if self.install(self.addon, ext):
return {'FINISHED'}
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): def download(self, addon, download_url):
filetype = os.path.splitext(download_url)[1] 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: except urllib.error.HTTPError as err:
log.warning("Download failed with HTTPError: %s %s", str(err.code), err.reason) log.warning("Download failed with HTTPError: %s %s", str(err.code), err.reason)
return False return False
except urllib.error.URLError as err:
log.warning("Download failed with URLError: %s", err)
return False
return True return True
@@ -184,7 +341,8 @@ class WM_OT_addon_download_install(bpy.types.Operator):
shutil.rmtree(addon_path) shutil.rmtree(addon_path)
with zipfile.ZipFile(download_path,"r") as zipped_addon: with zipfile.ZipFile(download_path,"r") as zipped_addon:
zipped_addon.extractall(bpy.utils.user_resource('SCRIPTS', path="addons")) zipped_addon.extractall(bpy.utils.user_resource('SCRIPTS', path="addons"))
else:
return False
return True return True