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):
|
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()
|
||||||
|
|
||||||
|
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.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
|
||||||
|
@@ -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):
|
|
||||||
|
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 {'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):
|
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
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user