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,39 +1,59 @@
import logging
from multiprocessing import Process, Pipe
from bpy.types import Operator
class SubprocessOperatorMixin:
timer = None
log = logging.getLogger("%s.SubprocessOperatorMixin" % __name__)
def subprocess_operator(cls: Operator, polling_interval=.01) -> Operator:
"""
Class decorator which wraps Operator methods with setup code for running a subprocess.
# run once in invoke
def setup(self):
pass
# run on receipt of data from subprocess
def handle_response(self, resp):
pass
Expects args for Process() to defined in cls.proc_args and cls.proc_kwargs. For example,
setting cls.proc_kwargs = {'target:' some_func} can be used to run 'some_func' in
a subprocess.
"""
def __init__(self):
self.pipe = Pipe()
self.subprocess = None
def decoratify(cls, methodname, decorator):
"""
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):
self.subprocess.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()
def decorate_execute(orig_execute):
def execute(self, context):
orig_execute(self, context)
call_copy_of_method_if_exist(cls, 'execute', self, context)
return self.invoke(context, None)
return execute
wm = context.window_manager
wm.modal_handler_add(self)
self.timer = wm.event_timer_add(.01, context.window)
def decorate_invoke(orig_invoke):
def invoke(self, context, event):
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")
try:
if self.pipe[0].poll():
@@ -45,6 +65,13 @@ class SubprocessOperatorMixin:
return {'FINISHED'}
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'}
decoratify(cls, 'execute', decorate_execute)
decoratify(cls, 'invoke', decorate_invoke)
setattr(cls, 'poll_subprocess', poll_subprocess)
return cls