2017-07-07 17:56:49 -07:00
|
|
|
import logging
|
|
|
|
from multiprocessing import Process, Pipe
|
2017-07-08 17:51:41 -07:00
|
|
|
from bpy.types import Operator
|
2017-07-07 17:56:49 -07:00
|
|
|
|
2017-07-09 14:40:56 -07:00
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
2017-07-08 17:51:41 -07:00
|
|
|
def subprocess_operator(cls: Operator, polling_interval=.01) -> Operator:
|
|
|
|
"""
|
|
|
|
Class decorator which wraps Operator methods with setup code for running a subprocess.
|
2017-07-07 17:56:49 -07:00
|
|
|
|
2017-07-08 17:51:41 -07:00
|
|
|
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.
|
|
|
|
"""
|
2017-07-07 17:56:49 -07:00
|
|
|
|
2017-07-08 17:51:41 -07:00
|
|
|
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))
|
2017-07-07 17:56:49 -07:00
|
|
|
|
|
|
|
|
2017-07-08 17:51:41 -07:00
|
|
|
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
|
2017-07-09 14:57:22 -07:00
|
|
|
|
2017-07-07 17:56:49 -07:00
|
|
|
|
2017-07-08 17:51:41 -07:00
|
|
|
def decorate_invoke(orig_invoke):
|
2017-07-09 14:49:34 -07:00
|
|
|
"""
|
|
|
|
Create pipe and modal timer, start subprocess
|
|
|
|
"""
|
2017-07-08 17:51:41 -07:00
|
|
|
def invoke(self, context, event):
|
2017-07-09 14:57:22 -07:00
|
|
|
self.pipe = Pipe() #HACK: do this first so operator-defined invoke can access it
|
|
|
|
# this is needed because the operator is currently responsible for setting up the Process,
|
|
|
|
# and the pipe is needed for the subprocess_function decorator.
|
|
|
|
# TODO: Perhaps this responsibility should be handled here instead (simplifies things but looses some flexibility)
|
2017-07-09 14:40:56 -07:00
|
|
|
|
2017-07-09 18:59:36 -07:00
|
|
|
orig_ret = orig_invoke(self, context, event)
|
|
|
|
|
|
|
|
# HACK to allow early exits.
|
|
|
|
# Perhaps subprocess_operators should define separately named methods, to avoid confusing mixing semantics in return types
|
|
|
|
if not orig_ret.issubset({'RUNNING_MODAL'}):
|
|
|
|
log.debug('Exiting early')
|
|
|
|
return orig_ret
|
2017-07-07 17:56:49 -07:00
|
|
|
|
2017-07-08 17:51:41 -07:00
|
|
|
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()
|
2017-07-07 17:56:49 -07:00
|
|
|
|
2017-07-08 17:51:41 -07:00
|
|
|
wm = context.window_manager
|
|
|
|
wm.modal_handler_add(self)
|
|
|
|
self.timer = wm.event_timer_add(polling_interval, context.window)
|
2017-07-07 17:56:49 -07:00
|
|
|
|
2017-07-08 17:51:41 -07:00
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
return invoke
|
|
|
|
|
|
|
|
def poll_subprocess(self):
|
2017-07-09 14:49:34 -07:00
|
|
|
"""
|
|
|
|
Check the pipe for new messages (nonblocking). If there is a new message, call the callback
|
|
|
|
"""
|
2017-07-09 18:59:36 -07:00
|
|
|
log.debug("polling")
|
2017-07-07 17:56:49 -07:00
|
|
|
try:
|
|
|
|
if self.pipe[0].poll():
|
2017-07-09 14:40:56 -07:00
|
|
|
resp = self.pipe[0].recv()
|
2017-07-09 15:10:00 -07:00
|
|
|
# assert(isinstance(resp, SubprocessMessage))
|
2017-07-07 17:56:49 -07:00
|
|
|
else:
|
2017-07-09 14:40:56 -07:00
|
|
|
resp = None
|
2017-07-07 17:56:49 -07:00
|
|
|
except EOFError:
|
2017-07-09 18:59:36 -07:00
|
|
|
log.debug("done polling")
|
2017-07-07 17:56:49 -07:00
|
|
|
return {'FINISHED'}
|
|
|
|
|
2017-07-09 14:40:56 -07:00
|
|
|
if resp is not None:
|
|
|
|
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
|
2017-07-08 17:51:41 -07:00
|
|
|
# this should allow chaining of multiple subprocess in a single operator
|
|
|
|
|
|
|
|
decoratify(cls, 'execute', decorate_execute)
|
|
|
|
decoratify(cls, 'invoke', decorate_invoke)
|
|
|
|
setattr(cls, 'poll_subprocess', poll_subprocess)
|
|
|
|
|
|
|
|
return cls
|
2017-07-09 14:40:56 -07:00
|
|
|
|
|
|
|
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))
|
2017-07-09 15:10:00 -07:00
|
|
|
raise
|
2017-07-09 14:40:56 -07:00
|
|
|
finally:
|
|
|
|
pipe[1].close()
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
class SubprocessMessage:
|
2017-07-09 14:49:34 -07:00
|
|
|
"""
|
|
|
|
Container for communications between child processes and the parent process
|
|
|
|
"""
|
|
|
|
#TODO: currently only used for child -> parent, might need something different for parent -> child?
|
2017-07-09 14:40:56 -07:00
|
|
|
def __init__(self, exception=None, data=None):
|
|
|
|
self.exception = exception
|
|
|
|
self.data = data
|