Add decorator for handing child-process side of pipe

This commit is contained in:
gandalf3
2017-07-09 14:40:56 -07:00
parent 1188f91b7b
commit 8cc606b263
3 changed files with 49 additions and 12 deletions

View File

@@ -34,6 +34,8 @@ def register():
return new_module return new_module
blender_common = reload_mod('blender_common') blender_common = reload_mod('blender_common')
subprocess_adapter = reload_mod('subprocess_adapter')
bpackage = reload_mod('bpackage')
else: else:
from . import blender_common from . import blender_common

View File

@@ -5,7 +5,7 @@ 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 subprocess_operator from .subprocess_adapter import subprocess_operator, subprocess_function
from . import bpackage as bpkg from . import bpackage as bpkg
class RepositoryProperty(PropertyGroup): class RepositoryProperty(PropertyGroup):
@@ -42,27 +42,30 @@ class PackagePreferences(AddonPreferences):
@subprocess_operator @subprocess_operator
class PACKAGE_OT_refresh(Operator): class PACKAGE_OT_refresh(Operator):
"""
Operator which checks for updates to known package lists
"""
bl_idname = "package.refresh" bl_idname = "package.refresh"
bl_label = "Update package list(s)" bl_label = "Refresh package list(s)"
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def invoke(self, context, event): def invoke(self, context, event):
prefs = context.user_preferences.addons[__package__].preferences prefs = context.user_preferences.addons[__package__].preferences
if 'repositories' not in prefs: if 'repositories' not in prefs:
return {'FINISHED'} return {'FINISHED'}
# HACK: just do the active repo for now # HACK: just do the active repo for now
repo = bpkg.Repository(prefs['repositories'][prefs.active_repository].to_dict()) repo = bpkg.Repository(prefs['repositories'][prefs.active_repository].to_dict())
self.proc_args = []
self.proc_kwargs = {'target': repo.refresh}
def execute(self, context, event): self.proc_kwargs = {'target': subprocess_function(repo.refresh, pipe=self.pipe)}
pass
def modal(self, context, event): def modal(self, context, event):
# try: # try:
self.poll_subprocess() self.poll_subprocess()
# except: # except:
return {'RUNNING_MODAL'}
def handle_response(self, resp): def handle_response(self, resp):
self.report({'INFO'}, "Request returned %s" % resp) self.report({'INFO'}, "Request returned %s" % resp)

View File

@@ -2,6 +2,8 @@ import logging
from multiprocessing import Process, Pipe from multiprocessing import Process, Pipe
from bpy.types import Operator from bpy.types import Operator
log = logging.getLogger(__name__)
def subprocess_operator(cls: Operator, polling_interval=.01) -> Operator: def subprocess_operator(cls: Operator, polling_interval=.01) -> Operator:
""" """
Class decorator which wraps Operator methods with setup code for running a subprocess. Class decorator which wraps Operator methods with setup code for running a subprocess.
@@ -34,9 +36,10 @@ def subprocess_operator(cls: Operator, polling_interval=.01) -> Operator:
def decorate_invoke(orig_invoke): def decorate_invoke(orig_invoke):
def invoke(self, context, event): def invoke(self, context, event):
self.pipe = Pipe()
orig_invoke(self, context, event) orig_invoke(self, context, event)
self.pipe = Pipe()
self.proc = Process( self.proc = Process(
*getattr(self, 'proc_args', []), *getattr(self, 'proc_args', []),
**getattr(self, 'proc_kwargs', []) **getattr(self, 'proc_kwargs', [])
@@ -57,15 +60,19 @@ def subprocess_operator(cls: Operator, polling_interval=.01) -> Operator:
self.log.debug("polling") self.log.debug("polling")
try: try:
if self.pipe[0].poll(): if self.pipe[0].poll():
newdata = self.pipe[0].recv() resp = self.pipe[0].recv()
assert(isinstance(resp, SubprocessMessage))
else: else:
newdata = None resp = None
except EOFError: except EOFError:
self.log.debug("done polling") self.log.debug("done polling")
return {'FINISHED'} return {'FINISHED'}
if newdata is not None: if resp is not None:
self.handle_response(newdata) #TODO: make this a customizable callback if resp.exception is not None:
raise resp.exception
elif resp.data is not None:
self.handle_response(resp.data) #TODO: make this a customizable callback
# this should allow chaining of multiple subprocess in a single operator # this should allow chaining of multiple subprocess in a single operator
return {'PASS_THROUGH'} return {'PASS_THROUGH'}
@@ -75,3 +82,28 @@ def subprocess_operator(cls: Operator, polling_interval=.01) -> Operator:
setattr(cls, 'poll_subprocess', poll_subprocess) setattr(cls, 'poll_subprocess', poll_subprocess)
return cls return cls
def subprocess_function(func, *args, pipe, **kwargs):
"""
Wrapper which feeds the return val of `func` into the latter end of a pipe
"""
def wrapper(*args, **kwargs):
# 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.
pipe[0].close()
try:
return_val = func(*args, **kwargs)
pipe[1].send(SubprocessMessage(data=return_val))
except Exception as err:
log.debug("Caught exception from subprocess: %s", err)
pipe[1].send(SubprocessMessage(exception=err))
finally:
pipe[1].close()
return wrapper
class SubprocessMessage:
def __init__(self, exception=None, data=None):
self.exception = exception
self.data = data