Support coroutines and async/await in the task manager and loader

This commit is contained in:
rdb 2017-10-31 15:45:55 +01:00
parent a7d68a8412
commit 0c0f9adab9
26 changed files with 821 additions and 119 deletions

View File

@ -21,31 +21,103 @@ class Loader(DirectObject):
loaderIndex = 0 loaderIndex = 0
class Callback: class Callback:
def __init__(self, numObjects, gotList, callback, extraArgs): """Returned by loadModel when used asynchronously. This class is
modelled after Future, and can be awaited."""
# This indicates that this class behaves like a Future.
_asyncio_future_blocking = False
def __init__(self, loader, numObjects, gotList, callback, extraArgs):
self._loader = loader
self.objects = [None] * numObjects self.objects = [None] * numObjects
self.gotList = gotList self.gotList = gotList
self.callback = callback self.callback = callback
self.extraArgs = extraArgs self.extraArgs = extraArgs
self.numRemaining = numObjects
self.cancelled = False
self.requests = set() self.requests = set()
self.requestList = []
def gotObject(self, index, object): def gotObject(self, index, object):
self.objects[index] = object self.objects[index] = object
self.numRemaining -= 1
if self.numRemaining == 0: if not self.requests:
if self.gotList: self._loader = None
self.callback(self.objects, *self.extraArgs) if self.callback:
else: if self.gotList:
self.callback(*(self.objects + self.extraArgs)) self.callback(self.objects, *self.extraArgs)
else:
self.callback(*(self.objects + self.extraArgs))
def cancel(self):
"Cancels the request. Callback won't be called."
if self._loader:
self._loader = None
for request in self.requests:
self._loader.loader.remove(request)
del self._loader._requests[request]
self.requests = None
self.requestList = None
def cancelled(self):
"Returns true if the request was cancelled."
return self.requestList is None
def done(self):
"Returns true if all the requests were finished or cancelled."
return not self.requests
def result(self):
assert not self.requests, "Result is not ready."
if self.gotList:
return self.objects
else:
return self.objects[0]
def exception(self):
assert self.done() and not self.cancelled()
return None
def __await__(self):
""" Returns a generator that raises StopIteration when the loading
is complete. This allows this class to be used with 'await'."""
if self.requests:
self._asyncio_future_blocking = True
yield self
# This should be a simple return, but older versions of Python
# don't allow return statements with arguments.
result = self.result()
exc = StopIteration(result)
exc.value = result
raise exc
def __aiter__(self):
""" This allows using `async for` to iterate asynchronously over
the results of this class. It does guarantee to return the
results in order, though, even though they may not be loaded in
that order. """
requestList = self.requestList
assert requestList is not None, "Request was cancelled."
class AsyncIter:
index = 0
def __anext__(self):
if self.index < len(requestList):
i = self.index
self.index = i + 1
return requestList[i]
else:
raise StopAsyncIteration
iter = AsyncIter()
iter.objects = self.objects
return iter
# special methods # special methods
def __init__(self, base): def __init__(self, base):
self.base = base self.base = base
self.loader = PandaLoader.getGlobalPtr() self.loader = PandaLoader.getGlobalPtr()
self.__requests = {} self._requests = {}
self.hook = "async_loader_%s" % (Loader.loaderIndex) self.hook = "async_loader_%s" % (Loader.loaderIndex)
Loader.loaderIndex += 1 Loader.loaderIndex += 1
@ -180,7 +252,7 @@ class Loader(DirectObject):
# requested models have been loaded, we'll invoke the # requested models have been loaded, we'll invoke the
# callback (passing it the models on the parameter list). # callback (passing it the models on the parameter list).
cb = Loader.Callback(len(modelList), gotList, callback, extraArgs) cb = Loader.Callback(self, len(modelList), gotList, callback, extraArgs)
i = 0 i = 0
for modelPath in modelList: for modelPath in modelList:
request = self.loader.makeAsyncRequest(Filename(modelPath), loaderOptions) request = self.loader.makeAsyncRequest(Filename(modelPath), loaderOptions)
@ -189,26 +261,26 @@ class Loader(DirectObject):
request.setDoneEvent(self.hook) request.setDoneEvent(self.hook)
self.loader.loadAsync(request) self.loader.loadAsync(request)
cb.requests.add(request) cb.requests.add(request)
self.__requests[request] = (cb, i) cb.requestList.append(request)
self._requests[request] = (cb, i)
i += 1 i += 1
return cb return cb
def cancelRequest(self, cb): def cancelRequest(self, cb):
"""Cancels an aysynchronous loading or flatten request issued """Cancels an aysynchronous loading or flatten request issued
earlier. The callback associated with the request will not be earlier. The callback associated with the request will not be
called after cancelRequest() has been performed. """ called after cancelRequest() has been performed.
if not cb.cancelled: This is now deprecated: call cb.cancel() instead. """
cb.cancelled = True
for request in cb.requests: cb.cancel()
self.loader.remove(request)
del self.__requests[request]
cb.requests = None
def isRequestPending(self, cb): def isRequestPending(self, cb):
""" Returns true if an asynchronous loading or flatten request """ Returns true if an asynchronous loading or flatten request
issued earlier is still pending, or false if it has completed or issued earlier is still pending, or false if it has completed or
been cancelled. """ been cancelled.
This is now deprecated: call cb.done() instead. """
return bool(cb.requests) return bool(cb.requests)
@ -344,7 +416,7 @@ class Loader(DirectObject):
# requested models have been saved, we'll invoke the # requested models have been saved, we'll invoke the
# callback (passing it the models on the parameter list). # callback (passing it the models on the parameter list).
cb = Loader.Callback(len(modelList), gotList, callback, extraArgs) cb = Loader.Callback(self, len(modelList), gotList, callback, extraArgs)
i = 0 i = 0
for modelPath, node in modelList: for modelPath, node in modelList:
request = self.loader.makeAsyncSaveRequest(Filename(modelPath), loaderOptions, node) request = self.loader.makeAsyncSaveRequest(Filename(modelPath), loaderOptions, node)
@ -353,7 +425,8 @@ class Loader(DirectObject):
request.setDoneEvent(self.hook) request.setDoneEvent(self.hook)
self.loader.saveAsync(request) self.loader.saveAsync(request)
cb.requests.add(request) cb.requests.add(request)
self.__requests[request] = (cb, i) cb.requestList.append(request)
self._requests[request] = (cb, i)
i += 1 i += 1
return cb return cb
@ -880,13 +953,14 @@ class Loader(DirectObject):
# requested sounds have been loaded, we'll invoke the # requested sounds have been loaded, we'll invoke the
# callback (passing it the sounds on the parameter list). # callback (passing it the sounds on the parameter list).
cb = Loader.Callback(len(soundList), gotList, callback, extraArgs) cb = Loader.Callback(self, len(soundList), gotList, callback, extraArgs)
for i, soundPath in enumerate(soundList): for i, soundPath in enumerate(soundList):
request = AudioLoadRequest(manager, soundPath, positional) request = AudioLoadRequest(manager, soundPath, positional)
request.setDoneEvent(self.hook) request.setDoneEvent(self.hook)
self.loader.loadAsync(request) self.loader.loadAsync(request)
cb.requests.add(request) cb.requests.add(request)
self.__requests[request] = (cb, i) cb.requestList.append(request)
self._requests[request] = (cb, i)
return cb return cb
def unloadSfx(self, sfx): def unloadSfx(self, sfx):
@ -944,14 +1018,15 @@ class Loader(DirectObject):
callback = self.__asyncFlattenDone callback = self.__asyncFlattenDone
gotList = True gotList = True
cb = Loader.Callback(len(modelList), gotList, callback, extraArgs) cb = Loader.Callback(self, len(modelList), gotList, callback, extraArgs)
i = 0 i = 0
for model in modelList: for model in modelList:
request = ModelFlattenRequest(model.node()) request = ModelFlattenRequest(model.node())
request.setDoneEvent(self.hook) request.setDoneEvent(self.hook)
self.loader.loadAsync(request) self.loader.loadAsync(request)
cb.requests.add(request) cb.requests.add(request)
self.__requests[request] = (cb, i) cb.requestList.append(request)
self._requests[request] = (cb, i)
i += 1 i += 1
return cb return cb
@ -980,36 +1055,22 @@ class Loader(DirectObject):
of loaded objects, and call the appropriate callback when it's of loaded objects, and call the appropriate callback when it's
time.""" time."""
if request not in self.__requests: if request not in self._requests:
return return
cb, i = self.__requests[request] cb, i = self._requests[request]
if cb.cancelled: if cb.cancelled():
# Shouldn't be here. # Shouldn't be here.
del self.__requests[request] del self._requests[request]
return return
cb.requests.discard(request) cb.requests.discard(request)
if not cb.requests: if not cb.requests:
del self.__requests[request] del self._requests[request]
object = None cb.gotObject(i, request.result() or None)
if hasattr(request, "getModel"):
node = request.getModel()
if node is not None:
object = NodePath(node)
elif hasattr(request, "getSound"):
object = request.getSound()
elif hasattr(request, "getSuccess"):
object = request.getSuccess()
cb.gotObject(i, object)
load_model = loadModel load_model = loadModel
cancel_request = cancelRequest
is_request_pending = isRequestPending
unload_model = unloadModel unload_model = unloadModel
save_model = saveModel save_model = saveModel
load_font = loadFont load_font = loadFont

View File

@ -333,6 +333,7 @@ class TaskManager:
funcOrTask - either an existing Task object (not already added funcOrTask - either an existing Task object (not already added
to the task manager), or a callable function object. If this to the task manager), or a callable function object. If this
is a function, a new Task object will be created and returned. is a function, a new Task object will be created and returned.
You may also pass in a coroutine object.
name - the name to assign to the Task. Required, unless you name - the name to assign to the Task. Required, unless you
are passing in a Task object that already has a name. are passing in a Task object that already has a name.
@ -385,6 +386,15 @@ class TaskManager:
task = funcOrTask task = funcOrTask
elif hasattr(funcOrTask, '__call__'): elif hasattr(funcOrTask, '__call__'):
task = PythonTask(funcOrTask) task = PythonTask(funcOrTask)
if name is None:
name = getattr(funcOrTask, '__qualname__', None) or \
getattr(funcOrTask, '__name__', None)
elif hasattr(funcOrTask, 'cr_await') or type(funcOrTask) == types.GeneratorType:
# It's a coroutine, or something emulating one.
task = PythonTask(funcOrTask)
if name is None:
name = getattr(funcOrTask, '__qualname__', None) or \
getattr(funcOrTask, '__name__', None)
else: else:
self.notify.error( self.notify.error(
'add: Tried to add a task that was not a Task or a func') 'add: Tried to add a task that was not a Task or a func')

View File

@ -846,7 +846,7 @@ setup_properties(const InterrogateFunction &ifunc, InterfaceMaker *interface_mak
} }
} else if (fname == "__iter__") { } else if (fname == "__iter__") {
if (_has_this && _parameters.size() == 1 && if ((int)_parameters.size() == first_param &&
TypeManager::is_pointer(_return_type->get_new_type())) { TypeManager::is_pointer(_return_type->get_new_type())) {
// It receives no parameters, and returns a pointer. // It receives no parameters, and returns a pointer.
_flags |= F_iter; _flags |= F_iter;

View File

@ -499,6 +499,24 @@ get_slotted_function_def(Object *obj, Function *func, FunctionRemap *remap,
} }
} }
if (method_name == "__await__") {
def._answer_location = "am_await";
def._wrapper_type = WT_no_params;
return true;
}
if (method_name == "__aiter__") {
def._answer_location = "am_aiter";
def._wrapper_type = WT_no_params;
return true;
}
if (method_name == "__anext__") {
def._answer_location = "am_anext";
def._wrapper_type = WT_no_params;
return true;
}
if (method_name == "operator ()") { if (method_name == "operator ()") {
def._answer_location = "tp_call"; def._answer_location = "tp_call";
def._wrapper_type = WT_none; def._wrapper_type = WT_none;
@ -2798,6 +2816,20 @@ write_module_class(ostream &out, Object *obj) {
out << "};\n\n"; out << "};\n\n";
} }
bool have_async = false;
if (has_parent_class || slots.count("am_await") != 0 ||
slots.count("am_aiter") != 0 ||
slots.count("am_anext") != 0) {
out << "#if PY_VERSION_HEX >= 0x03050000\n";
out << "static PyAsyncMethods Dtool_AsyncMethods_" << ClassName << " = {\n";
write_function_slot(out, 2, slots, "am_await");
write_function_slot(out, 2, slots, "am_aiter");
write_function_slot(out, 2, slots, "am_anext");
out << "};\n";
out << "#endif\n\n";
have_async = true;
}
// Output the actual PyTypeObject definition. // Output the actual PyTypeObject definition.
out << "struct Dtool_PyTypedObject Dtool_" << ClassName << " = {\n"; out << "struct Dtool_PyTypedObject Dtool_" << ClassName << " = {\n";
out << " {\n"; out << " {\n";
@ -2819,7 +2851,13 @@ write_module_class(ostream &out, Object *obj) {
write_function_slot(out, 4, slots, "tp_setattr"); write_function_slot(out, 4, slots, "tp_setattr");
// cmpfunc tp_compare; (reserved in Python 3) // cmpfunc tp_compare; (reserved in Python 3)
out << "#if PY_MAJOR_VERSION >= 3\n"; out << "#if PY_VERSION_HEX >= 0x03050000\n";
if (have_async) {
out << " &Dtool_AsyncMethods_" << ClassName << ",\n";
} else {
out << " 0, // tp_as_async\n";
}
out << "#elif PY_MAJOR_VERSION >= 3\n";
out << " 0, // tp_reserved\n"; out << " 0, // tp_reserved\n";
out << "#else\n"; out << "#else\n";
if (has_hash_compare) { if (has_hash_compare) {

View File

@ -622,6 +622,10 @@ PyObject *Dtool_PyModuleInitHelper(LibraryDef *defs[], const char *modulename) {
return Dtool_Raise_TypeError("PyType_Ready(Dtool_SeqMapWrapper)"); return Dtool_Raise_TypeError("PyType_Ready(Dtool_SeqMapWrapper)");
} }
if (PyType_Ready(&Dtool_GeneratorWrapper_Type) < 0) {
return Dtool_Raise_TypeError("PyType_Ready(Dtool_GeneratorWrapper)");
}
if (PyType_Ready(&Dtool_StaticProperty_Type) < 0) { if (PyType_Ready(&Dtool_StaticProperty_Type) < 0) {
return Dtool_Raise_TypeError("PyType_Ready(Dtool_StaticProperty_Type)"); return Dtool_Raise_TypeError("PyType_Ready(Dtool_StaticProperty_Type)");
} }
@ -1111,6 +1115,13 @@ static int Dtool_SeqMapWrapper_setitem(PyObject *self, PyObject *key, PyObject *
} }
} }
static PyObject *Dtool_GeneratorWrapper_iternext(PyObject *self) {
Dtool_GeneratorWrapper *wrap = (Dtool_GeneratorWrapper *)self;
nassertr(wrap, nullptr);
nassertr(wrap->_iternext_func, nullptr);
return wrap->_iternext_func(wrap->_base._self);
}
static PySequenceMethods Dtool_SequenceWrapper_SequenceMethods = { static PySequenceMethods Dtool_SequenceWrapper_SequenceMethods = {
Dtool_SequenceWrapper_length, Dtool_SequenceWrapper_length,
0, // sq_concat 0, // sq_concat
@ -1454,4 +1465,66 @@ PyTypeObject Dtool_SeqMapWrapper_Type = {
#endif #endif
}; };
/**
* This variant defines only a generator interface.
*/
PyTypeObject Dtool_GeneratorWrapper_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"generator wrapper",
sizeof(Dtool_GeneratorWrapper),
0, // tp_itemsize
Dtool_WrapperBase_dealloc,
0, // tp_print
0, // tp_getattr
0, // tp_setattr
#if PY_MAJOR_VERSION >= 3
0, // tp_reserved
#else
0, // tp_compare
#endif
0, // tp_repr
0, // tp_as_number
0, // tp_as_sequence
0, // tp_as_mapping
0, // tp_hash
0, // tp_call
0, // tp_str
PyObject_GenericGetAttr,
PyObject_GenericSetAttr,
0, // tp_as_buffer
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES,
0, // tp_doc
0, // tp_traverse
0, // tp_clear
0, // tp_richcompare
0, // tp_weaklistoffset
PyObject_SelfIter,
Dtool_GeneratorWrapper_iternext,
0, // tp_methods
0, // tp_members
0, // tp_getset
0, // tp_base
0, // tp_dict
0, // tp_descr_get
0, // tp_descr_set
0, // tp_dictoffset
0, // tp_init
PyType_GenericAlloc,
0, // tp_new
PyObject_Del,
0, // tp_is_gc
0, // tp_bases
0, // tp_mro
0, // tp_cache
0, // tp_subclasses
0, // tp_weaklist
0, // tp_del
#if PY_VERSION_HEX >= 0x02060000
0, // tp_version_tag
#endif
#if PY_VERSION_HEX >= 0x03040000
0, // tp_finalize
#endif
};
#endif // HAVE_PYTHON #endif // HAVE_PYTHON

View File

@ -495,9 +495,15 @@ struct Dtool_SeqMapWrapper {
objobjargproc _map_setitem_func; objobjargproc _map_setitem_func;
}; };
struct Dtool_GeneratorWrapper {
Dtool_WrapperBase _base;
iternextfunc _iternext_func;
};
EXPCL_INTERROGATEDB extern PyTypeObject Dtool_SequenceWrapper_Type; EXPCL_INTERROGATEDB extern PyTypeObject Dtool_SequenceWrapper_Type;
EXPCL_INTERROGATEDB extern PyTypeObject Dtool_MappingWrapper_Type; EXPCL_INTERROGATEDB extern PyTypeObject Dtool_MappingWrapper_Type;
EXPCL_INTERROGATEDB extern PyTypeObject Dtool_SeqMapWrapper_Type; EXPCL_INTERROGATEDB extern PyTypeObject Dtool_SeqMapWrapper_Type;
EXPCL_INTERROGATEDB extern PyTypeObject Dtool_GeneratorWrapper_Type;
EXPCL_INTERROGATEDB extern PyTypeObject Dtool_StaticProperty_Type; EXPCL_INTERROGATEDB extern PyTypeObject Dtool_StaticProperty_Type;
EXPCL_INTERROGATEDB PyObject *Dtool_NewStaticProperty(PyTypeObject *obj, const PyGetSetDef *getset); EXPCL_INTERROGATEDB PyObject *Dtool_NewStaticProperty(PyTypeObject *obj, const PyGetSetDef *getset);

View File

@ -115,6 +115,7 @@ extern "C" {
EXPCL_PYSTUB int PyObject_IsTrue(...); EXPCL_PYSTUB int PyObject_IsTrue(...);
EXPCL_PYSTUB int PyObject_Repr(...); EXPCL_PYSTUB int PyObject_Repr(...);
EXPCL_PYSTUB int PyObject_RichCompareBool(...); EXPCL_PYSTUB int PyObject_RichCompareBool(...);
EXPCL_PYSTUB int PyObject_SelfIter(...);
EXPCL_PYSTUB int PyObject_SetAttrString(...); EXPCL_PYSTUB int PyObject_SetAttrString(...);
EXPCL_PYSTUB int PyObject_Str(...); EXPCL_PYSTUB int PyObject_Str(...);
EXPCL_PYSTUB int PyObject_Type(...); EXPCL_PYSTUB int PyObject_Type(...);
@ -336,6 +337,7 @@ int PyObject_IsInstance(...) { return 0; }
int PyObject_IsTrue(...) { return 0; } int PyObject_IsTrue(...) { return 0; }
int PyObject_Repr(...) { return 0; } int PyObject_Repr(...) { return 0; }
int PyObject_RichCompareBool(...) { return 0; } int PyObject_RichCompareBool(...) { return 0; }
int PyObject_SelfIter(...) { return 0; }
int PyObject_SetAttrString(...) { return 0; } int PyObject_SetAttrString(...) { return 0; }
int PyObject_Str(...) { return 0; } int PyObject_Str(...) { return 0; }
int PyObject_Type(...) { return 0; } int PyObject_Type(...) { return 0; }

View File

@ -3616,6 +3616,7 @@ if (not RUNTIME):
TargetAdd('p3event_composite2.obj', opts=OPTS, input='p3event_composite2.cxx') TargetAdd('p3event_composite2.obj', opts=OPTS, input='p3event_composite2.cxx')
OPTS=['DIR:panda/src/event', 'PYTHON'] OPTS=['DIR:panda/src/event', 'PYTHON']
TargetAdd('p3event_asyncTask_ext.obj', opts=OPTS, input='asyncTask_ext.cxx')
TargetAdd('p3event_pythonTask.obj', opts=OPTS, input='pythonTask.cxx') TargetAdd('p3event_pythonTask.obj', opts=OPTS, input='pythonTask.cxx')
IGATEFILES=GetDirectoryContents('panda/src/event', ["*.h", "*_composite*.cxx"]) IGATEFILES=GetDirectoryContents('panda/src/event', ["*.h", "*_composite*.cxx"])
TargetAdd('libp3event.in', opts=OPTS, input=IGATEFILES) TargetAdd('libp3event.in', opts=OPTS, input=IGATEFILES)
@ -4209,6 +4210,7 @@ if (not RUNTIME):
TargetAdd('core.pyd', input='p3pipeline_pythonThread.obj') TargetAdd('core.pyd', input='p3pipeline_pythonThread.obj')
TargetAdd('core.pyd', input='p3putil_ext_composite.obj') TargetAdd('core.pyd', input='p3putil_ext_composite.obj')
TargetAdd('core.pyd', input='p3pnmimage_pfmFile_ext.obj') TargetAdd('core.pyd', input='p3pnmimage_pfmFile_ext.obj')
TargetAdd('core.pyd', input='p3event_asyncTask_ext.obj')
TargetAdd('core.pyd', input='p3event_pythonTask.obj') TargetAdd('core.pyd', input='p3event_pythonTask.obj')
TargetAdd('core.pyd', input='p3gobj_ext_composite.obj') TargetAdd('core.pyd', input='p3gobj_ext_composite.obj')
TargetAdd('core.pyd', input='p3pgraph_ext_composite.obj') TargetAdd('core.pyd', input='p3pgraph_ext_composite.obj')

View File

@ -62,11 +62,24 @@ is_ready() const {
} }
/** /**
* Returns the sound that was loaded asynchronously, if any, or NULL if there * Returns the sound that was loaded asynchronously, if any, or nullptr if
* was an error. It is an error to call this unless is_ready() returns true. * there was an error. It is an error to call this unless is_ready() returns
* true.
* @deprecated Use result() instead.
*/ */
INLINE AudioSound *AudioLoadRequest:: INLINE AudioSound *AudioLoadRequest::
get_sound() const { get_sound() const {
nassertr(_is_ready, NULL); nassertr(_is_ready, NULL);
return _sound; return _sound;
} }
/**
* Returns the sound that was loaded asynchronously, if any, or nullptr if
* there was an error. It is an error to call this unless is_ready() returns
* true.
*/
INLINE AudioSound *AudioLoadRequest::
result() const {
nassertr(_is_ready, nullptr);
return _sound;
}

View File

@ -42,6 +42,8 @@ PUBLISHED:
INLINE bool is_ready() const; INLINE bool is_ready() const;
INLINE AudioSound *get_sound() const; INLINE AudioSound *get_sound() const;
INLINE AudioSound *result() const;
protected: protected:
virtual DoneStatus do_task(); virtual DoneStatus do_task();

View File

@ -33,6 +33,7 @@ is_alive() const {
case S_servicing: case S_servicing:
case S_sleeping: case S_sleeping:
case S_active_nested: case S_active_nested:
case S_awaiting:
return true; return true;
case S_inactive: case S_inactive:

View File

@ -45,6 +45,7 @@ PUBLISHED:
DS_exit, // stop the enclosing sequence DS_exit, // stop the enclosing sequence
DS_pause, // pause, then exit (useful within a sequence) DS_pause, // pause, then exit (useful within a sequence)
DS_interrupt, // interrupt the task manager, but run task again DS_interrupt, // interrupt the task manager, but run task again
DS_await, // await a different task's completion
}; };
enum State { enum State {
@ -54,6 +55,7 @@ PUBLISHED:
S_servicing_removed, // Still servicing, but wants removal from manager. S_servicing_removed, // Still servicing, but wants removal from manager.
S_sleeping, S_sleeping,
S_active_nested, // active within a sequence. S_active_nested, // active within a sequence.
S_awaiting, // Waiting for a dependent task to complete
}; };
INLINE State get_state() const; INLINE State get_state() const;
@ -98,6 +100,9 @@ PUBLISHED:
virtual void output(ostream &out) const; virtual void output(ostream &out) const;
EXTENSION(static PyObject *__await__(PyObject *self));
EXTENSION(static PyObject *__iter__(PyObject *self));
protected: protected:
void jump_to_task_chain(AsyncTaskManager *manager); void jump_to_task_chain(AsyncTaskManager *manager);
DoneStatus unlock_and_do_task(); DoneStatus unlock_and_do_task();
@ -130,11 +135,16 @@ protected:
double _total_dt; double _total_dt;
int _num_frames; int _num_frames;
// Tasks waiting for this one to complete.
pvector<PT(AsyncTask)> _waiting_tasks;
static AtomicAdjust::Integer _next_task_id; static AtomicAdjust::Integer _next_task_id;
static PStatCollector _show_code_pcollector; static PStatCollector _show_code_pcollector;
PStatCollector _task_pcollector; PStatCollector _task_pcollector;
friend class PythonTask;
public: public:
static TypeHandle get_class_type() { static TypeHandle get_class_type() {
return _type_handle; return _type_handle;

View File

@ -44,6 +44,7 @@ AsyncTaskChain(AsyncTaskManager *manager, const string &name) :
_frame_sync(false), _frame_sync(false),
_num_busy_threads(0), _num_busy_threads(0),
_num_tasks(0), _num_tasks(0),
_num_awaiting_tasks(0),
_state(S_initial), _state(S_initial),
_current_sort(-INT_MAX), _current_sort(-INT_MAX),
_pickup_mode(false), _pickup_mode(false),
@ -726,6 +727,13 @@ service_one_task(AsyncTaskChain::AsyncTaskChainThread *thread) {
} }
break; break;
case AsyncTask::DS_await:
// The task wants to wait for another one to finish.
task->_state = AsyncTask::S_awaiting;
_cvar.notify_all();
++_num_awaiting_tasks;
break;
default: default:
// The task has finished. // The task has finished.
cleanup_task(task, true, true); cleanup_task(task, true, true);
@ -775,6 +783,20 @@ cleanup_task(AsyncTask *task, bool upon_death, bool clean_exit) {
_manager->remove_task_by_name(task); _manager->remove_task_by_name(task);
// Activate the tasks that were waiting for this one to finish.
if (upon_death) {
pvector<PT(AsyncTask)>::iterator it;
for (it = task->_waiting_tasks.begin(); it != task->_waiting_tasks.end(); ++it) {
AsyncTask *task = *it;
// Note that this task may not be on the same task chain.
nassertd(task->_manager == _manager) continue;
task->_state = AsyncTask::S_active;
task->_chain->_active.push_back(task);
--task->_chain->_num_awaiting_tasks;
}
task->_waiting_tasks.clear();
}
if (upon_death) { if (upon_death) {
_manager->_lock.release(); _manager->_lock.release();
task->upon_death(_manager, clean_exit); task->upon_death(_manager, clean_exit);
@ -899,7 +921,7 @@ finish_sort_group() {
filter_timeslice_priority(); filter_timeslice_priority();
} }
nassertr((size_t)_num_tasks == _active.size() + _this_active.size() + _next_active.size() + _sleeping.size(), true); nassertr((size_t)_num_tasks == _active.size() + _this_active.size() + _next_active.size() + _sleeping.size() + (size_t)_num_awaiting_tasks, true);
make_heap(_active.begin(), _active.end(), AsyncTaskSortPriority()); make_heap(_active.begin(), _active.end(), AsyncTaskSortPriority());
_current_sort = -INT_MAX; _current_sort = -INT_MAX;

View File

@ -172,6 +172,7 @@ protected:
bool _frame_sync; bool _frame_sync;
int _num_busy_threads; int _num_busy_threads;
int _num_tasks; int _num_tasks;
int _num_awaiting_tasks;
TaskHeap _active; TaskHeap _active;
TaskHeap _this_active; TaskHeap _this_active;
TaskHeap _next_active; TaskHeap _next_active;

View File

@ -155,6 +155,7 @@ private:
friend class AsyncTaskChain::AsyncTaskChainThread; friend class AsyncTaskChain::AsyncTaskChainThread;
friend class AsyncTask; friend class AsyncTask;
friend class AsyncTaskSequence; friend class AsyncTaskSequence;
friend class PythonTask;
}; };
INLINE ostream &operator << (ostream &out, const AsyncTaskManager &manager) { INLINE ostream &operator << (ostream &out, const AsyncTaskManager &manager) {

View File

@ -0,0 +1,75 @@
/**
* PANDA 3D SOFTWARE
* Copyright (c) Carnegie Mellon University. All rights reserved.
*
* All use of this software is subject to the terms of the revised BSD
* license. You should have received a copy of this license along
* with this source code in a file named "LICENSE."
*
* @file asyncTask_ext.h
* @author rdb
* @date 2017-10-29
*/
#include "asyncTask_ext.h"
#include "nodePath.h"
#ifdef HAVE_PYTHON
#ifndef CPPPARSER
extern struct Dtool_PyTypedObject Dtool_AsyncTask;
#endif
/**
* Yields continuously until the task has finished.
*/
static PyObject *gen_next(PyObject *self) {
const AsyncTask *request = nullptr;
if (!Dtool_Call_ExtractThisPointer(self, Dtool_AsyncTask, (void **)&request)) {
return nullptr;
}
if (request->is_alive()) {
// Continue awaiting the result.
Py_INCREF(self);
return self;
} else {
// It's done. Do we have a method like result(), eg. in the case of a
// ModelLoadRequest? In that case we pass that value into the exception.
PyObject *method = PyObject_GetAttrString(self, "result");
PyObject *result = nullptr;
if (method != nullptr) {
if (PyCallable_Check(method)) {
result = _PyObject_CallNoArg(method);
Py_DECREF(method);
if (result == nullptr) {
// An exception happened. Pass it on.
return nullptr;
}
}
Py_DECREF(method);
}
Py_INCREF(PyExc_StopIteration);
PyErr_Restore(PyExc_StopIteration, result, nullptr);
return nullptr;
}
}
/**
* Returns a generator that continuously yields an awaitable until the task
* has finished. This allows syntax like `model = await loader.load...` to be
* used in a Python coroutine.
*/
PyObject *Extension<AsyncTask>::
__await__(PyObject *self) {
Dtool_GeneratorWrapper *gen;
gen = (Dtool_GeneratorWrapper *)PyType_GenericAlloc(&Dtool_GeneratorWrapper_Type, 0);
if (gen != nullptr) {
Py_INCREF(self);
gen->_base._self = self;
gen->_iternext_func = &gen_next;
}
return (PyObject *)gen;
}
#endif

35
panda/src/event/asyncTask_ext.h Executable file
View File

@ -0,0 +1,35 @@
/**
* PANDA 3D SOFTWARE
* Copyright (c) Carnegie Mellon University. All rights reserved.
*
* All use of this software is subject to the terms of the revised BSD
* license. You should have received a copy of this license along
* with this source code in a file named "LICENSE."
*
* @file asyncTask_ext.h
* @author rdb
* @date 2017-10-29
*/
#ifndef ASYNCTASK_EXT_H
#define ASYNCTASK_EXT_H
#include "extension.h"
#include "py_panda.h"
#include "modelLoadRequest.h"
#ifdef HAVE_PYTHON
/**
* Extension class for AsyncTask
*/
template<>
class Extension<AsyncTask> : public ExtensionBase<AsyncTask> {
public:
static PyObject *__await__(PyObject *self);
static PyObject *__iter__(PyObject *self) { return __await__(self); }
};
#endif // HAVE_PYTHON
#endif // ASYNCTASK_EXT_H

View File

@ -10,3 +10,52 @@
* @author drose * @author drose
* @date 2008-09-16 * @date 2008-09-16
*/ */
/**
* Returns the function that is called when the task runs.
*/
INLINE PyObject *PythonTask::
get_function() {
Py_INCREF(_function);
return _function;
}
/**
* Returns the function that is called when the task finishes.
*/
INLINE PyObject *PythonTask::
get_upon_death() {
Py_INCREF(_upon_death);
return _upon_death;
}
/**
* Returns the "owner" object. See set_owner().
*/
INLINE PyObject *PythonTask::
get_owner() const {
Py_INCREF(_owner);
return _owner;
}
/**
* Sets the "result" of this task. This is the value returned from an "await"
* expression on this task.
* This can only be called while the task is still alive.
*/
INLINE void PythonTask::
set_result(PyObject *result) {
nassertv(is_alive());
nassertv(_exception == nullptr);
Py_INCREF(result);
Py_XDECREF(_exc_value);
_exc_value = result;
}
/**
* Same as __await__, for backward compatibility with the old coroutine way.
*/
INLINE PyObject *PythonTask::
__iter__(PyObject *self) {
return __await__(self);
}

View File

@ -19,28 +19,52 @@
#include "py_panda.h" #include "py_panda.h"
#include "pythonThread.h" #include "pythonThread.h"
#include "asyncTaskManager.h"
TypeHandle PythonTask::_type_handle; TypeHandle PythonTask::_type_handle;
#ifndef CPPPARSER #ifndef CPPPARSER
extern struct Dtool_PyTypedObject Dtool_TypedReferenceCount; extern struct Dtool_PyTypedObject Dtool_TypedReferenceCount;
extern struct Dtool_PyTypedObject Dtool_AsyncTask;
extern struct Dtool_PyTypedObject Dtool_PythonTask;
#endif #endif
/** /**
* *
*/ */
PythonTask:: PythonTask::
PythonTask(PyObject *function, const string &name) : PythonTask(PyObject *func_or_coro, const string &name) :
AsyncTask(name) AsyncTask(name),
{ _function(nullptr),
_function = NULL; _args(nullptr),
_args = NULL; _upon_death(nullptr),
_upon_death = NULL; _owner(nullptr),
_owner = NULL; _registered_to_owner(false),
_registered_to_owner = false; _exception(nullptr),
_generator = NULL; _exc_value(nullptr),
_exc_traceback(nullptr),
_generator(nullptr),
_future_done(nullptr),
_retrieved_exception(false) {
nassertv(func_or_coro != nullptr);
if (func_or_coro == Py_None || PyCallable_Check(func_or_coro)) {
_function = func_or_coro;
Py_INCREF(_function);
#if PY_VERSION_HEX >= 0x03050000
} else if (PyCoro_CheckExact(func_or_coro)) {
// We also allow passing in a coroutine, because why not.
_generator = func_or_coro;
Py_INCREF(_generator);
#endif
} else if (PyGen_CheckExact(func_or_coro)) {
// Something emulating a coroutine.
_generator = func_or_coro;
Py_INCREF(_generator);
} else {
nassert_raise("Invalid function passed to PythonTask");
}
set_function(function);
set_args(Py_None, true); set_args(Py_None, true);
set_upon_death(Py_None); set_upon_death(Py_None);
set_owner(Py_None); set_owner(Py_None);
@ -60,9 +84,24 @@ PythonTask(PyObject *function, const string &name) :
*/ */
PythonTask:: PythonTask::
~PythonTask() { ~PythonTask() {
Py_DECREF(_function); #ifndef NDEBUG
// If the coroutine threw an exception, and there was no opportunity to
// handle it, let the user know.
if (_exception != nullptr && !_retrieved_exception) {
task_cat.error()
<< *this << " exception was never retrieved:\n";
PyErr_Restore(_exception, _exc_value, _exc_traceback);
PyErr_Print();
PyErr_Restore(nullptr, nullptr, nullptr);
}
#endif
Py_XDECREF(_function);
Py_DECREF(_args); Py_DECREF(_args);
Py_DECREF(__dict__); Py_DECREF(__dict__);
Py_XDECREF(_exception);
Py_XDECREF(_exc_value);
Py_XDECREF(_exc_traceback);
Py_XDECREF(_generator); Py_XDECREF(_generator);
Py_XDECREF(_owner); Py_XDECREF(_owner);
Py_XDECREF(_upon_death); Py_XDECREF(_upon_death);
@ -83,15 +122,6 @@ set_function(PyObject *function) {
} }
} }
/**
* Returns the function that is called when the task runs.
*/
PyObject *PythonTask::
get_function() {
Py_INCREF(_function);
return _function;
}
/** /**
* Replaces the argument list that is passed to the task function. The * Replaces the argument list that is passed to the task function. The
* parameter should be a tuple or list of arguments, or None to indicate the * parameter should be a tuple or list of arguments, or None to indicate the
@ -166,15 +196,6 @@ set_upon_death(PyObject *upon_death) {
} }
} }
/**
* Returns the function that is called when the task finishes.
*/
PyObject *PythonTask::
get_upon_death() {
Py_INCREF(_upon_death);
return _upon_death;
}
/** /**
* Specifies a Python object that serves as the "owner" for the task. This * Specifies a Python object that serves as the "owner" for the task. This
* owner object must have two methods: _addTask() and _clearTask(), which will * owner object must have two methods: _addTask() and _clearTask(), which will
@ -212,12 +233,87 @@ set_owner(PyObject *owner) {
} }
/** /**
* Returns the "owner" object. See set_owner(). * Returns the result of this task's execution, as set by set_result() within
* the task or returned from a coroutine added to the task manager. If an
* exception occurred within this task, it is raised instead.
*/ */
PyObject *PythonTask:: PyObject *PythonTask::
get_owner() { result() const {
Py_INCREF(_owner); nassertr(!is_alive(), nullptr);
return _owner;
if (_exception == nullptr) {
// The result of the call is stored in _exc_value.
Py_XINCREF(_exc_value);
return _exc_value;
} else {
_retrieved_exception = true;
Py_INCREF(_exception);
Py_XINCREF(_exc_value);
Py_XINCREF(_exc_traceback);
PyErr_Restore(_exception, _exc_value, _exc_traceback);
return nullptr;
}
}
/**
* If an exception occurred during execution of this task, returns it. This
* is only set if this task returned a coroutine or generator.
*/
/*PyObject *PythonTask::
exception() const {
if (_exception == nullptr) {
Py_INCREF(Py_None);
return Py_None;
} else if (_exc_value == nullptr || _exc_value == Py_None) {
return _PyObject_CallNoArg(_exception);
} else if (PyTuple_Check(_exc_value)) {
return PyObject_Call(_exception, _exc_value, nullptr);
} else {
return PyObject_CallFunctionObjArgs(_exception, _exc_value, nullptr);
}
}*/
/**
* Returns an iterator that continuously yields an awaitable until the task
* has finished.
*/
PyObject *PythonTask::
__await__(PyObject *self) {
Dtool_GeneratorWrapper *gen;
gen = (Dtool_GeneratorWrapper *)PyType_GenericAlloc(&Dtool_GeneratorWrapper_Type, 0);
if (gen != nullptr) {
Py_INCREF(self);
gen->_base._self = self;
gen->_iternext_func = &gen_next;
}
return (PyObject *)gen;
}
/**
* Yields continuously until a task has finished.
*/
PyObject *PythonTask::
gen_next(PyObject *self) {
const PythonTask *task = nullptr;
if (!Dtool_Call_ExtractThisPointer(self, Dtool_PythonTask, (void **)&task)) {
return nullptr;
}
if (task->is_alive()) {
Py_INCREF(self);
return self;
} else if (task->_exception != nullptr) {
task->_retrieved_exception = true;
Py_INCREF(task->_exception);
Py_INCREF(task->_exc_value);
Py_INCREF(task->_exc_traceback);
PyErr_Restore(task->_exception, task->_exc_value, task->_exc_traceback);
return nullptr;
} else {
// The result of the call is stored in _exc_value.
PyErr_SetObject(PyExc_StopIteration, task->_exc_value);
return nullptr;
}
} }
/** /**
@ -396,16 +492,30 @@ do_task() {
*/ */
AsyncTask::DoneStatus PythonTask:: AsyncTask::DoneStatus PythonTask::
do_python_task() { do_python_task() {
PyObject *result = NULL; PyObject *result = nullptr;
if (_generator == (PyObject *)NULL) { // Are we waiting for a future to finish?
if (_future_done != nullptr) {
PyObject *is_done = PyObject_CallObject(_future_done, nullptr);
if (!PyObject_IsTrue(is_done)) {
// Nope, ask again next frame.
Py_DECREF(is_done);
return DS_cont;
}
Py_DECREF(is_done);
Py_DECREF(_future_done);
_future_done = nullptr;
}
if (_generator == nullptr) {
// We are calling the function directly. // We are calling the function directly.
nassertr(_function != nullptr, DS_interrupt);
PyObject *args = get_args(); PyObject *args = get_args();
result = PythonThread::call_python_func(_function, args); result = PythonThread::call_python_func(_function, args);
Py_DECREF(args); Py_DECREF(args);
#ifdef PyGen_Check if (result != nullptr && PyGen_Check(result)) {
if (result != (PyObject *)NULL && PyGen_Check(result)) {
// The function has yielded a generator. We will call into that // The function has yielded a generator. We will call into that
// henceforth, instead of calling the function from the top again. // henceforth, instead of calling the function from the top again.
if (task_cat.is_debug()) { if (task_cat.is_debug()) {
@ -423,30 +533,166 @@ do_python_task() {
Py_DECREF(str); Py_DECREF(str);
} }
_generator = result; _generator = result;
result = NULL; result = nullptr;
}
#if PY_VERSION_HEX >= 0x03050000
} else if (result != nullptr && Py_TYPE(result)->tp_as_async != nullptr) {
// The function yielded a coroutine, or something of the sort.
if (task_cat.is_debug()) {
PyObject *str = PyObject_ASCII(_function);
PyObject *str2 = PyObject_ASCII(result);
task_cat.debug()
<< PyUnicode_AsUTF8(str) << " in " << *this
<< " yielded an awaitable: " << PyUnicode_AsUTF8(str2) << "\n";
Py_DECREF(str);
Py_DECREF(str2);
}
if (PyCoro_CheckExact(result)) {
// If a coroutine, am_await is possible but senseless, since we can
// just call send(None) on the coroutine itself.
_generator = result;
} else {
unaryfunc await = Py_TYPE(result)->tp_as_async->am_await;
_generator = await(result);
Py_DECREF(result);
}
result = nullptr;
#endif #endif
}
} }
if (_generator != (PyObject *)NULL) { if (_generator != nullptr) {
// We are calling a generator. // We are calling a generator. Use "send" rather than PyIter_Next since
PyObject *func = PyObject_GetAttrString(_generator, "next"); // we need to be able to read the value from a StopIteration exception.
nassertr(func != (PyObject *)NULL, DS_interrupt); PyObject *func = PyObject_GetAttrString(_generator, "send");
nassertr(func != nullptr, DS_interrupt);
result = PyObject_CallObject(func, NULL); result = PyObject_CallFunctionObjArgs(func, Py_None, nullptr);
Py_DECREF(func); Py_DECREF(func);
if (result == (PyObject *)NULL && PyErr_Occurred() && if (result == nullptr) {
PyErr_ExceptionMatches(PyExc_StopIteration)) { // An error happened. If StopIteration, that indicates the task has
// "Catch" StopIteration and treat it like DS_done. // returned. Otherwise, we need to save it so that it can be re-raised
PyErr_Clear(); // in the function that awaited this task.
Py_DECREF(_generator); Py_DECREF(_generator);
_generator = NULL; _generator = nullptr;
return DS_done;
#if PY_VERSION_HEX >= 0x03030000
if (_PyGen_FetchStopIterationValue(&result) == 0) {
#else
if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
result = Py_None;
Py_INCREF(result);
#endif
PyErr_Restore(nullptr, nullptr, nullptr);
// If we passed a coroutine into the task, eg. something like:
// taskMgr.add(my_async_function())
// then we cannot rerun the task, so the return value is always
// assumed to be DS_done. Instead, we pass the return value to the
// result of the `await` expression.
if (_function == nullptr) {
if (task_cat.is_debug()) {
task_cat.debug()
<< *this << " received StopIteration from coroutine.\n";
}
// Store the result in _exc_value because that's not used anyway.
Py_XDECREF(_exc_value);
_exc_value = result;
return DS_done;
}
} else if (_function == nullptr) {
// We got an exception. If this is a scheduled coroutine, we will
// keep it and instead throw it into whatever 'awaits' this task.
// Otherwise, fall through and handle it the regular way.
Py_XDECREF(_exception);
Py_XDECREF(_exc_value);
Py_XDECREF(_exc_traceback);
PyErr_Fetch(&_exception, &_exc_value, &_exc_traceback);
_retrieved_exception = false;
if (task_cat.is_debug()) {
if (_exception != nullptr && Py_TYPE(_exception) == &PyType_Type) {
task_cat.debug()
<< *this << " received " << ((PyTypeObject *)_exception)->tp_name << " from coroutine.\n";
} else {
task_cat.debug()
<< *this << " received exception from coroutine.\n";
}
}
// Tell the task chain we want to kill ourselves. It doesn't really
// matter what we return if we set S_servicing_removed. If we don't
// set it, however, it will think this was a clean exit.
_manager->_lock.acquire();
_state = S_servicing_removed;
_manager->_lock.release();
return DS_interrupt;
}
} else if (DtoolCanThisBeAPandaInstance(result)) {
// We are waiting for a task to finish.
void *ptr = ((Dtool_PyInstDef *)result)->_My_Type->_Dtool_UpcastInterface(result, &Dtool_AsyncTask);
if (ptr != nullptr) {
// Suspend execution of this task until this other task has completed.
AsyncTask *task = (AsyncTask *)ptr;
AsyncTaskManager *manager = task->_manager;
nassertr(manager != nullptr, DS_interrupt);
nassertr(manager == _manager, DS_interrupt);
manager->_lock.acquire();
if (task != (AsyncTask *)this) {
if (task->is_alive()) {
if (task_cat.is_debug()) {
task_cat.debug()
<< *this << " is now awaiting <" << *task << ">.\n";
}
task->_waiting_tasks.push_back(this);
} else {
// The task is already done. Continue at next opportunity.
Py_DECREF(result);
manager->_lock.release();
return DS_cont;
}
} else {
// This is an error. If we wanted to be fancier we could also
// detect deeper circular dependencies.
task_cat.error()
<< *this << " cannot await itself\n";
}
task->_manager->_lock.release();
Py_DECREF(result);
return DS_await;
}
} else {
// We are waiting for a future to finish. We currently implement this
// by simply checking every frame whether the future is done.
PyObject *check = PyObject_GetAttrString(result, "_asyncio_future_blocking");
if (check != nullptr && check != Py_None) {
Py_DECREF(check);
// Next frame, check whether this future is done.
_future_done = PyObject_GetAttrString(result, "done");
if (_future_done == nullptr || !PyCallable_Check(_future_done)) {
task_cat.error()
<< "future.done is not callable\n";
return DS_interrupt;
}
#if PY_MAJOR_VERSION >= 3
if (task_cat.is_debug()) {
PyObject *str = PyObject_ASCII(result);
task_cat.debug()
<< *this << " is now awaiting " << PyUnicode_AsUTF8(str) << ".\n";
Py_DECREF(str);
}
#endif
Py_DECREF(result);
return DS_cont;
}
PyErr_Clear();
Py_XDECREF(check);
} }
} }
if (result == (PyObject *)NULL) { if (result == nullptr) {
if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_SystemExit)) { if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_SystemExit)) {
// Don't print an error message for SystemExit. Or rather, make it a // Don't print an error message for SystemExit. Or rather, make it a
// debug message. // debug message.

View File

@ -22,8 +22,8 @@
#include "py_panda.h" #include "py_panda.h"
/** /**
* This class exists to allow association of a Python function with the * This class exists to allow association of a Python function or coroutine
* AsyncTaskManager. * with the AsyncTaskManager.
*/ */
class PythonTask : public AsyncTask { class PythonTask : public AsyncTask {
PUBLISHED: PUBLISHED:
@ -32,16 +32,23 @@ PUBLISHED:
ALLOC_DELETED_CHAIN(PythonTask); ALLOC_DELETED_CHAIN(PythonTask);
void set_function(PyObject *function); void set_function(PyObject *function);
PyObject *get_function(); INLINE PyObject *get_function();
void set_args(PyObject *args, bool append_task); void set_args(PyObject *args, bool append_task);
PyObject *get_args(); PyObject *get_args();
void set_upon_death(PyObject *upon_death); void set_upon_death(PyObject *upon_death);
PyObject *get_upon_death(); INLINE PyObject *get_upon_death();
void set_owner(PyObject *owner); void set_owner(PyObject *owner);
PyObject *get_owner(); INLINE PyObject *get_owner() const;
INLINE void set_result(PyObject *result);
PyObject *result() const;
//PyObject *exception() const;
static PyObject *__await__(PyObject *self);
INLINE static PyObject *__iter__(PyObject *self);
int __setattr__(PyObject *self, PyObject *attr, PyObject *v); int __setattr__(PyObject *self, PyObject *attr, PyObject *v);
int __delattr__(PyObject *self, PyObject *attr); int __delattr__(PyObject *self, PyObject *attr);
@ -94,6 +101,8 @@ protected:
virtual void upon_death(AsyncTaskManager *manager, bool clean_exit); virtual void upon_death(AsyncTaskManager *manager, bool clean_exit);
private: private:
static PyObject *gen_next(PyObject *self);
void register_to_owner(); void register_to_owner();
void unregister_from_owner(); void unregister_from_owner();
void call_owner_method(const char *method_name); void call_owner_method(const char *method_name);
@ -102,12 +111,19 @@ private:
private: private:
PyObject *_function; PyObject *_function;
PyObject *_args; PyObject *_args;
bool _append_task;
PyObject *_upon_death; PyObject *_upon_death;
PyObject *_owner; PyObject *_owner;
bool _registered_to_owner;
PyObject *_exception;
PyObject *_exc_value;
PyObject *_exc_traceback;
PyObject *_generator; PyObject *_generator;
PyObject *_future_done;
bool _append_task;
bool _registered_to_owner;
mutable bool _retrieved_exception;
public: public:
static TypeHandle get_class_type() { static TypeHandle get_class_type() {

View File

@ -34,7 +34,7 @@ get_orig() const {
/** /**
* Returns true if this request has completed, false if it is still pending. * Returns true if this request has completed, false if it is still pending.
* When this returns true, you may retrieve the model loaded by calling * When this returns true, you may retrieve the model loaded by calling
* get_result(). * result().
*/ */
INLINE bool ModelFlattenRequest:: INLINE bool ModelFlattenRequest::
is_ready() const { is_ready() const {
@ -47,6 +47,20 @@ is_ready() const {
*/ */
INLINE PandaNode *ModelFlattenRequest:: INLINE PandaNode *ModelFlattenRequest::
get_model() const { get_model() const {
nassertr(_is_ready, NULL); nassertr(_is_ready, nullptr);
return _model; return _model;
} }
/**
* Returns the flattened copy of the model wrapped in a NodePath. It is an
* error to call this unless is_ready() returns true.
*/
INLINE NodePath ModelFlattenRequest::
result() const {
nassertr(_is_ready, NodePath::fail());
if (_model != nullptr) {
return NodePath(_model);
} else {
return NodePath::fail();
}
}

View File

@ -19,6 +19,7 @@
#include "asyncTask.h" #include "asyncTask.h"
#include "pandaNode.h" #include "pandaNode.h"
#include "pointerTo.h" #include "pointerTo.h"
#include "nodePath.h"
/** /**
* This class object manages a single asynchronous request to flatten a model. * This class object manages a single asynchronous request to flatten a model.
@ -38,9 +39,10 @@ PUBLISHED:
INLINE bool is_ready() const; INLINE bool is_ready() const;
INLINE PandaNode *get_model() const; INLINE PandaNode *get_model() const;
INLINE NodePath result() const;
MAKE_PROPERTY(orig, get_orig); MAKE_PROPERTY(orig, get_orig);
MAKE_PROPERTY(ready, is_ready); MAKE_PROPERTY(ready, is_ready);
MAKE_PROPERTY(model, get_model);
protected: protected:
virtual DoneStatus do_task(); virtual DoneStatus do_task();

View File

@ -37,6 +37,16 @@ get_loader() const {
return _loader; return _loader;
} }
/**
* Returns the model that was loaded asynchronously as a NodePath, if any, or
* the empty NodePath if there was an error.
*/
INLINE NodePath ModelLoadRequest::
result() const {
nassertr_always(_is_ready, NodePath::fail());
return NodePath(_model);
}
/** /**
* Returns true if this request has completed, false if it is still pending. * Returns true if this request has completed, false if it is still pending.
* When this returns true, you may retrieve the model loaded by calling * When this returns true, you may retrieve the model loaded by calling

View File

@ -11,8 +11,8 @@
* @date 2006-08-29 * @date 2006-08-29
*/ */
#ifndef MODELLOADREQUEST #ifndef MODELLOADREQUEST_H
#define MODELLOADREQUEST #define MODELLOADREQUEST_H
#include "pandabase.h" #include "pandabase.h"
@ -22,6 +22,7 @@
#include "pandaNode.h" #include "pandaNode.h"
#include "pointerTo.h" #include "pointerTo.h"
#include "loader.h" #include "loader.h"
#include "nodePath.h"
/** /**
* A class object that manages a single asynchronous model load request. * A class object that manages a single asynchronous model load request.
@ -42,6 +43,8 @@ PUBLISHED:
INLINE const LoaderOptions &get_options() const; INLINE const LoaderOptions &get_options() const;
INLINE Loader *get_loader() const; INLINE Loader *get_loader() const;
INLINE NodePath result() const;
INLINE bool is_ready() const; INLINE bool is_ready() const;
INLINE PandaNode *get_model() const; INLINE PandaNode *get_model() const;
@ -49,7 +52,6 @@ PUBLISHED:
MAKE_PROPERTY(options, get_options); MAKE_PROPERTY(options, get_options);
MAKE_PROPERTY(loader, get_loader); MAKE_PROPERTY(loader, get_loader);
MAKE_PROPERTY(ready, is_ready); MAKE_PROPERTY(ready, is_ready);
MAKE_PROPERTY(model, get_model);
protected: protected:
virtual DoneStatus do_task(); virtual DoneStatus do_task();

View File

@ -64,3 +64,13 @@ get_success() const {
nassertr(_is_ready, false); nassertr(_is_ready, false);
return _success; return _success;
} }
/**
* Returns a boolean indicating whether the model saved correctly. It is an
* error to call this unless is_ready() returns true.
*/
INLINE bool ModelSaveRequest::
result() const {
nassertr(_is_ready, false);
return _success;
}

View File

@ -11,8 +11,8 @@
* @date 2012-12-19 * @date 2012-12-19
*/ */
#ifndef MODELSAVEREQUEST #ifndef MODELSAVEREQUEST_H
#define MODELSAVEREQUEST #define MODELSAVEREQUEST_H
#include "pandabase.h" #include "pandabase.h"
@ -46,12 +46,13 @@ PUBLISHED:
INLINE bool is_ready() const; INLINE bool is_ready() const;
INLINE bool get_success() const; INLINE bool get_success() const;
INLINE bool result() const;
MAKE_PROPERTY(filename, get_filename); MAKE_PROPERTY(filename, get_filename);
MAKE_PROPERTY(options, get_options); MAKE_PROPERTY(options, get_options);
MAKE_PROPERTY(node, get_node); MAKE_PROPERTY(node, get_node);
MAKE_PROPERTY(loader, get_loader); MAKE_PROPERTY(loader, get_loader);
MAKE_PROPERTY(ready, is_ready); MAKE_PROPERTY(ready, is_ready);
MAKE_PROPERTY(success, get_success);
protected: protected:
virtual DoneStatus do_task(); virtual DoneStatus do_task();