Subprocess management: use a decorator

Use a decorator instead of a mixin to handle subprocess spawning and
monitoring
This commit is contained in:
gandalf3
2017-07-08 17:51:41 -07:00
parent fcf90a0e75
commit 1188f91b7b
5 changed files with 99 additions and 49 deletions

View File

@@ -1,13 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is for code dealing specifically with blender # This file is for code dealing specifically with blender
import logging
import bpy import bpy
from bpy.props import CollectionProperty from bpy.props import CollectionProperty
from bpy.types import PropertyGroup, Panel, UIList, AddonPreferences, Operator from bpy.types import PropertyGroup, Panel, UIList, AddonPreferences, Operator
from .subprocess_adapter import SubprocessOperatorMixin from .subprocess_adapter import subprocess_operator
from . import bpackage as bpkg
class RepositoryProperty(PropertyGroup): class RepositoryProperty(PropertyGroup):
url = bpy.props.StringProperty(name="URL") url = bpy.props.StringProperty(name="URL")
# status = bpy.props.EnumProperty(name="Status")
class PACKAGE_UL_repositories(UIList): class PACKAGE_UL_repositories(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
@@ -32,23 +36,37 @@ class PackagePreferences(AddonPreferences):
col.operator("package.add_repository", icon="ZOOMIN", text="") col.operator("package.add_repository", icon="ZOOMIN", text="")
col.operator("package.remove_repository", icon="ZOOMOUT", text="") col.operator("package.remove_repository", icon="ZOOMOUT", text="")
class PACKAGE_OT_fetch(SubprocessOperatorMixin, bpy.types.Operator): row = layout.row()
bl_idname = "package.fetch" row.operator("package.refresh")
@subprocess_operator
class PACKAGE_OT_refresh(Operator):
bl_idname = "package.refresh"
bl_label = "Update package list(s)" bl_label = "Update package list(s)"
last_response = None log = logging.getLogger(__name__)
def __init__(self): def invoke(self, context, event):
super().__init__() prefs = context.user_preferences.addons[__package__].preferences
settings = bpy.context.window_manager.package_manager_settings if 'repositories' not in prefs:
self.subprocess = Process(target=blenderpack.fetch, args=(settings.url, self.pipe)) return {'FINISHED'}
# HACK: just do the active repo for now
repo = bpkg.Repository(prefs['repositories'][prefs.active_repository].to_dict())
self.proc_args = []
self.proc_kwargs = {'target': repo.refresh}
def execute(self, context, event):
pass
def modal(self, context, event):
# try:
self.poll_subprocess()
# except:
def handle_response(self, resp): def handle_response(self, resp):
self.__class__.last_response = resp self.report({'INFO'}, "Request returned %s" % resp)
self.report({'INFO'}, "Request returned %s" % self.__class__.last_response)
def execute(self, context):
return {'FINISHED'}
class PACKAGE_OT_add_repository(bpy.types.Operator): class PACKAGE_OT_add_repository(bpy.types.Operator):
bl_idname = "package.add_repository" bl_idname = "package.add_repository"
@@ -73,7 +91,7 @@ def register():
bpy.utils.register_class(RepositoryProperty) bpy.utils.register_class(RepositoryProperty)
bpy.utils.register_class(PackagePreferences) bpy.utils.register_class(PackagePreferences)
bpy.utils.register_class(PACKAGE_OT_fetch) bpy.utils.register_class(PACKAGE_OT_refresh)
bpy.utils.register_class(PACKAGE_OT_add_repository) bpy.utils.register_class(PACKAGE_OT_add_repository)
bpy.utils.register_class(PACKAGE_OT_remove_repository) bpy.utils.register_class(PACKAGE_OT_remove_repository)
@@ -83,7 +101,7 @@ def unregister():
bpy.utils.unregister_class(RepositoryProperty) bpy.utils.unregister_class(RepositoryProperty)
bpy.utils.unregister_class(PackagePreferences) bpy.utils.unregister_class(PackagePreferences)
bpy.utils.unregister_class(PACKAGE_OT_fetch) bpy.utils.unregister_class(PACKAGE_OT_refresh)
bpy.utils.unregister_class(PACKAGE_OT_add_repository) bpy.utils.unregister_class(PACKAGE_OT_add_repository)
bpy.utils.unregister_class(PACKAGE_OT_remove_repository) bpy.utils.unregister_class(PACKAGE_OT_remove_repository)

View File

@@ -1,3 +1,3 @@
import bpackage.exceptions from . import exceptions
from .package import Package from .package import Package
from .repository import Repository from .repository import Repository

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# HACK: seems 'requests' module bundled with blender isn't bundled with 'idna' module. So force system python for now # HACK: seems 'requests' module bundled with blender isn't bundled with 'idna' module. So force system python for now
# import sys import sys
# sys.path.insert(0, '/usr/lib/python3.6/site-packages') sys.path.insert(0, '/usr/lib/python3.6/site-packages')
import logging import logging

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# HACK: seems 'requests' module bundled with blender isn't bundled with 'idna' module. So force system python for now # HACK: seems 'requests' module bundled with blender isn't bundled with 'idna' module. So force system python for now
# import sys import sys
# sys.path.insert(0, '/usr/lib/python3.6/site-packages') sys.path.insert(0, '/usr/lib/python3.6/site-packages')
from pathlib import Path from pathlib import Path
import requests import requests
@@ -56,10 +56,15 @@ class Repository:
req_headers = {} req_headers = {}
# Do things this way to avoid adding empty objects/None to the req_headers dict # Do things this way to avoid adding empty objects/None to the req_headers dict
if 'etag' in self._headers: if self._headers:
req_headers['If-None-Match'] = self._headers['etag'] try:
if 'last-modified' in self._headers: req_headers['If-None-Match'] = self._headers['etag']
req_headers['If-Modified-Since'] = self._headers['last-modified'] except KeyError:
pass
try:
req_headers['If-Modified-Since'] = self._headers['last-modified']
except KeyError:
pass
#try #try
resp = requests.get(self.url, headers=req_headers) resp = requests.get(self.url, headers=req_headers)

View File

@@ -1,39 +1,59 @@
import logging import logging
from multiprocessing import Process, Pipe from multiprocessing import Process, Pipe
from bpy.types import Operator
class SubprocessOperatorMixin: def subprocess_operator(cls: Operator, polling_interval=.01) -> Operator:
timer = None """
log = logging.getLogger("%s.SubprocessOperatorMixin" % __name__) Class decorator which wraps Operator methods with setup code for running a subprocess.
# run once in invoke Expects args for Process() to defined in cls.proc_args and cls.proc_kwargs. For example,
def setup(self): setting cls.proc_kwargs = {'target:' some_func} can be used to run 'some_func' in
pass a subprocess.
# run on receipt of data from subprocess """
def handle_response(self, resp):
pass
def __init__(self): def decoratify(cls, methodname, decorator):
self.pipe = Pipe() """
self.subprocess = None Decorate `cls.methodname` with `decorator` if method `methodname` exists in cls
else just call the decorator with an empty function
"""
orig_method = getattr(cls, methodname, None)
if orig_method:
setattr(cls, methodname, decorator(orig_method))
else:
# HACK: need a no-op which accepts any arguments
setattr(cls, methodname, decorator(lambda *args, **kwargs: None))
def execute(self, context):
return self.invoke(context, None)
def invoke(self, context, event): def decorate_execute(orig_execute):
self.subprocess.start() def execute(self, context):
# we have to explicitly close the end of the pipe we are NOT using, orig_execute(self, context)
# otherwise no exception will be generated when the other process closes its end. call_copy_of_method_if_exist(cls, 'execute', self, context)
self.pipe[1].close() return self.invoke(context, None)
return execute
wm = context.window_manager def decorate_invoke(orig_invoke):
wm.modal_handler_add(self) def invoke(self, context, event):
self.timer = wm.event_timer_add(.01, context.window) orig_invoke(self, context, event)
self.setup() self.pipe = Pipe()
self.proc = Process(
*getattr(self, 'proc_args', []),
**getattr(self, 'proc_kwargs', [])
)
self.proc.start()
# we have to explicitly close the end of the pipe we are NOT using,
# otherwise no exception will be generated when the other process closes its end.
self.pipe[1].close()
return {'RUNNING_MODAL'} wm = context.window_manager
wm.modal_handler_add(self)
self.timer = wm.event_timer_add(polling_interval, context.window)
def modal(self, context, event): return {'RUNNING_MODAL'}
return invoke
def poll_subprocess(self):
self.log.debug("polling") self.log.debug("polling")
try: try:
if self.pipe[0].poll(): if self.pipe[0].poll():
@@ -45,6 +65,13 @@ class SubprocessOperatorMixin:
return {'FINISHED'} return {'FINISHED'}
if newdata is not None: if newdata is not None:
self.handle_response(newdata) self.handle_response(newdata) #TODO: make this a customizable callback
# this should allow chaining of multiple subprocess in a single operator
return {'PASS_THROUGH'} return {'PASS_THROUGH'}
decoratify(cls, 'execute', decorate_execute)
decoratify(cls, 'invoke', decorate_invoke)
setattr(cls, 'poll_subprocess', poll_subprocess)
return cls