async JavaScript

This commit is contained in:
David Rose 2009-07-16 18:30:17 +00:00
parent 8aa18e2e9c
commit aa8285f5a4
5 changed files with 136 additions and 67 deletions

View File

@ -168,11 +168,13 @@ run_python() {
Py_DECREF(runp3d); Py_DECREF(runp3d);
// Construct a Python wrapper around our request_func() method. // Construct a Python wrapper around our methods we need to expose to Python.
static PyMethodDef p3dpython_methods[] = { static PyMethodDef p3dpython_methods[] = {
{"request_func", P3DPythonRun::st_request_func, METH_VARARGS, { "check_comm", P3DPythonRun::st_check_comm, METH_VARARGS,
"Send an asynchronous request to the plugin host"}, "Poll for communications from the parent process" },
{NULL, NULL, 0, NULL} /* Sentinel */ { "request_func", P3DPythonRun::st_request_func, METH_VARARGS,
"Send an asynchronous request to the plugin host" },
{ NULL, NULL, 0, NULL } /* Sentinel */
}; };
PyObject *p3dpython = Py_InitModule("p3dpython", p3dpython_methods); PyObject *p3dpython = Py_InitModule("p3dpython", p3dpython_methods);
if (p3dpython == NULL) { if (p3dpython == NULL) {
@ -196,11 +198,31 @@ run_python() {
Py_DECREF(request_func); Py_DECREF(request_func);
// Now add check_comm() as a task. // Now add check_comm() as a task. It can be a threaded task, but
_check_comm_task = new GenericAsyncTask("check_comm", task_check_comm, this); // this does mean that application programmers will have to be alert
// to asynchronous calls coming in from JavaScript. We'll put it on
// its own task chain so the application programmer can decide how
// it should be.
AsyncTaskManager *task_mgr = AsyncTaskManager::get_global_ptr(); AsyncTaskManager *task_mgr = AsyncTaskManager::get_global_ptr();
PT(AsyncTaskChain) chain = task_mgr->make_task_chain("JavaScript");
chain->set_num_threads(1);
chain->set_thread_priority(TP_low);
PyObject *check_comm = PyObject_GetAttrString(p3dpython, "check_comm");
if (check_comm == NULL) {
PyErr_Print();
return false;
}
// We have to make it a PythonTask, not just a GenericAsyncTask,
// because we need the code in PythonTask that supports calling into
// Python from a separate thread.
_check_comm_task = new PythonTask(check_comm, "check_comm");
_check_comm_task->set_task_chain("JavaScript");
task_mgr->add(_check_comm_task); task_mgr->add(_check_comm_task);
Py_DECREF(check_comm);
// Finally, get lost in taskMgr.run(). // Finally, get lost in taskMgr.run().
PyObject *done = PyObject_CallMethod(_taskMgr, (char *)"run", (char *)""); PyObject *done = PyObject_CallMethod(_taskMgr, (char *)"run", (char *)"");
if (done == NULL) { if (done == NULL) {
@ -216,11 +238,14 @@ run_python() {
// Function: P3DPythonRun::handle_command // Function: P3DPythonRun::handle_command
// Access: Private // Access: Private
// Description: Handles a command received from the plugin host, via // Description: Handles a command received from the plugin host, via
// an XML syntax on the wire. // an XML syntax on the wire. Ownership of the XML
// document object is passed into this method.
//
// It's important *not* to be holding _commands_lock
// when calling this method.
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
void P3DPythonRun:: void P3DPythonRun::
handle_command(TiXmlDocument *doc) { handle_command(TiXmlDocument *doc) {
nout << "received: " << *doc << "\n" << flush;
TiXmlElement *xcommand = doc->FirstChildElement("command"); TiXmlElement *xcommand = doc->FirstChildElement("command");
if (xcommand != NULL) { if (xcommand != NULL) {
bool needs_response = false; bool needs_response = false;
@ -281,9 +306,15 @@ handle_command(TiXmlDocument *doc) {
handle_pyobj_command(xcommand, needs_response, want_response_id); handle_pyobj_command(xcommand, needs_response, want_response_id);
} else if (strcmp(cmd, "script_response") == 0) { } else if (strcmp(cmd, "script_response") == 0) {
// Response from a script request. // Response from a script request. In this case, we just
assert(!needs_response); // store it away instead of processing it immediately.
nout << "Ignoring unexpected script_response\n";
MutexHolder holder(_responses_lock);
_responses.push_back(doc);
// And now we must return out, instead of deleting the
// document at the bottom of this method.
return;
} else if (strcmp(cmd, "drop_pyobj") == 0) { } else if (strcmp(cmd, "drop_pyobj") == 0) {
int object_id; int object_id;
@ -312,6 +343,8 @@ handle_command(TiXmlDocument *doc) {
} }
} }
} }
delete doc;
} }
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
@ -560,45 +593,40 @@ handle_pyobj_command(TiXmlElement *xcommand, bool needs_response,
// Function: P3DPythonRun::check_comm // Function: P3DPythonRun::check_comm
// Access: Private // Access: Private
// Description: This method is added to the task manager (via // Description: This method is added to the task manager (via
// task_check_comm, below) so that it gets a call every // st_check_comm, below) so that it gets a call every
// frame. Its job is to check for commands received // frame. Its job is to check for commands received
// from the plugin host in the parent process. // from the plugin host in the parent process.
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
void P3DPythonRun:: void P3DPythonRun::
check_comm() { check_comm() {
nout << ":" << flush; // nout << ":" << flush;
ACQUIRE_LOCK(_commands_lock); ACQUIRE_LOCK(_commands_lock);
while (!_commands.empty()) { while (!_commands.empty()) {
TiXmlDocument *doc = _commands.front(); TiXmlDocument *doc = _commands.front();
_commands.pop_front(); _commands.pop_front();
assert(_commands.size() < 10);
RELEASE_LOCK(_commands_lock); RELEASE_LOCK(_commands_lock);
handle_command(doc); handle_command(doc);
delete doc;
ACQUIRE_LOCK(_commands_lock); ACQUIRE_LOCK(_commands_lock);
} }
RELEASE_LOCK(_commands_lock);
if (!_program_continue) { if (!_program_continue) {
// The low-level thread detected an error, for instance pipe // The low-level thread detected an error, for instance pipe
// closed. We should exit gracefully. // closed. We should exit gracefully.
terminate_session(); terminate_session();
} }
RELEASE_LOCK(_commands_lock);
} }
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
// Function: P3DPythonRun::task_check_comm // Function: P3DPythonRun::st_check_comm
// Access: Private, Static // Access: Private, Static
// Description: This static function wrapper around check_comm is // Description: This is a static Python wrapper around py_check_comm,
// necessary to add the method function to the // needed to add the function to a PythonTask.
// GenericAsyncTask object.
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
AsyncTask::DoneStatus P3DPythonRun:: PyObject *P3DPythonRun::
task_check_comm(GenericAsyncTask *task, void *user_data) { st_check_comm(PyObject *, PyObject *args) {
P3DPythonRun *self = (P3DPythonRun *)user_data; P3DPythonRun::_global_ptr->check_comm();
self->check_comm(); return Py_BuildValue("i", AsyncTask::DS_cont);
return AsyncTask::DS_cont;
} }
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
@ -611,54 +639,63 @@ task_check_comm(GenericAsyncTask *task, void *user_data) {
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
TiXmlDocument *P3DPythonRun:: TiXmlDocument *P3DPythonRun::
wait_script_response(int response_id) { wait_script_response(int response_id) {
nout << "waiting script_response " << response_id << "\n" << flush; // nout << "waiting script_response " << response_id << "\n" << flush;
while (true) { while (true) {
ACQUIRE_LOCK(_commands_lock);
Commands::iterator ci; Commands::iterator ci;
// First, walk through the _commands queue to see if there's
// anything that needs immediate processing.
ACQUIRE_LOCK(_commands_lock);
for (ci = _commands.begin(); ci != _commands.end(); ++ci) { for (ci = _commands.begin(); ci != _commands.end(); ++ci) {
TiXmlDocument *doc = (*ci); TiXmlDocument *doc = (*ci);
TiXmlElement *xcommand = doc->FirstChildElement("command"); TiXmlElement *xcommand = doc->FirstChildElement("command");
if (xcommand != NULL) { if (xcommand != NULL) {
const char *cmd = xcommand->Attribute("cmd"); const char *cmd = xcommand->Attribute("cmd");
if (cmd != NULL && strcmp(cmd, "script_response") == 0) { if ((cmd != NULL && strcmp(cmd, "script_response") == 0) ||
int unique_id; xcommand->Attribute("want_response_id") != NULL) {
if (xcommand->QueryIntAttribute("unique_id", &unique_id) == TIXML_SUCCESS) {
if (unique_id == response_id) {
// This is the response we were waiting for.
_commands.erase(ci);
RELEASE_LOCK(_commands_lock);
nout << "got script_response " << unique_id << "\n" << flush;
return doc;
}
}
}
// It's not the response we're waiting for, but maybe we need // This is either a response, or it's a command that will
// to handle it anyway. // want a response itself. In either case we should handle
bool needs_response = false; // it right away. ("handling" a response means moving it to
int want_response_id; // the _responses queue.)
if (xcommand->QueryIntAttribute("want_response_id", &want_response_id) == TIXML_SUCCESS) {
// This command will be wanting a response. We'd better
// honor it right away, or we risk deadlock with the browser
// process and the Python process waiting for each other.
nout << "honoring response " << want_response_id << "\n" << flush;
_commands.erase(ci); _commands.erase(ci);
RELEASE_LOCK(_commands_lock); RELEASE_LOCK(_commands_lock);
handle_command(doc); handle_command(doc);
delete doc;
ACQUIRE_LOCK(_commands_lock); ACQUIRE_LOCK(_commands_lock);
break; break;
} }
} }
} }
RELEASE_LOCK(_commands_lock);
// Now, walk through the _responses queue to look for the
// particular response we're waiting for.
_responses_lock.acquire();
for (ci = _responses.begin(); ci != _responses.end(); ++ci) {
TiXmlDocument *doc = (*ci);
TiXmlElement *xcommand = doc->FirstChildElement("command");
assert(xcommand != NULL);
const char *cmd = xcommand->Attribute("cmd");
assert(cmd != NULL && strcmp(cmd, "script_response") == 0);
int unique_id;
if (xcommand->QueryIntAttribute("unique_id", &unique_id) == TIXML_SUCCESS) {
if (unique_id == response_id) {
// This is the response we were waiting for.
_responses.erase(ci);
_responses_lock.release();
// nout << "got script_response " << unique_id << "\n" << flush;
return doc;
}
}
}
_responses_lock.release();
if (!_program_continue) { if (!_program_continue) {
terminate_session(); terminate_session();
} }
RELEASE_LOCK(_commands_lock);
#ifdef _WIN32 #ifdef _WIN32
// Make sure we process the Windows event loop while we're // Make sure we process the Windows event loop while we're
@ -671,7 +708,7 @@ wait_script_response(int response_id) {
PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE | PM_NOYIELD); PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE | PM_NOYIELD);
#endif // _WIN32 #endif // _WIN32
nout << "." << flush; // nout << "." << flush;
// It hasn't shown up yet. Give the sub-thread a chance to // It hasn't shown up yet. Give the sub-thread a chance to
// process the input and append it to the queue. // process the input and append it to the queue.

View File

@ -25,9 +25,10 @@
#include "p3d_lock.h" #include "p3d_lock.h"
#include "handleStream.h" #include "handleStream.h"
#include "p3dCInstance.h" #include "p3dCInstance.h"
#include "genericAsyncTask.h" #include "pythonTask.h"
#include "pmap.h" #include "pmap.h"
#include "pdeque.h" #include "pdeque.h"
#include "pmutex.h"
#include "get_tinyxml.h" #include "get_tinyxml.h"
#include <Python.h> #include <Python.h>
@ -75,7 +76,7 @@ private:
void handle_script_response_command(TiXmlElement *xcommand); void handle_script_response_command(TiXmlElement *xcommand);
void check_comm(); void check_comm();
static AsyncTask::DoneStatus task_check_comm(GenericAsyncTask *task, void *user_data); static PyObject *st_check_comm(PyObject *, PyObject *args);
TiXmlDocument *wait_script_response(int response_id); TiXmlDocument *wait_script_response(int response_id);
PyObject *py_request_func(PyObject *args); PyObject *py_request_func(PyObject *args);
@ -118,7 +119,7 @@ private:
PyObject *_browser_object_class; PyObject *_browser_object_class;
PyObject *_taskMgr; PyObject *_taskMgr;
PT(GenericAsyncTask) _check_comm_task; PT(PythonTask) _check_comm_task;
// This map keeps track of the PyObject pointers we have delivered // This map keeps track of the PyObject pointers we have delivered
// to the parent process. We have to hold the reference count on // to the parent process. We have to hold the reference count on
@ -128,8 +129,14 @@ private:
SentObjects _sent_objects; SentObjects _sent_objects;
int _next_sent_id; int _next_sent_id;
// The remaining members are manipulated by the read thread.
typedef pdeque<TiXmlDocument *> Commands; typedef pdeque<TiXmlDocument *> Commands;
// This is a special queue of responses extracted from the _commands
// queue, below. It's protected by the Panda mutex.
Commands _responses;
Mutex _responses_lock;
// The remaining members are manipulated by the read thread.
Commands _commands; Commands _commands;
// This has to be an actual OS LOCK instead of Panda's Mutex, // This has to be an actual OS LOCK instead of Panda's Mutex,

View File

@ -311,6 +311,7 @@ class Messenger:
if taskChain: if taskChain:
# Queue the event onto the indicated task chain. # Queue the event onto the indicated task chain.
from direct.task.TaskManagerGlobal import taskMgr
taskMgr.add(self.__lockAndDispatch, name = 'Messenger-%s-%s' % (event, taskChain), extraArgs = [acceptorDict, event, sentArgs, foundWatch], taskChain = taskChain) taskMgr.add(self.__lockAndDispatch, name = 'Messenger-%s-%s' % (event, taskChain), extraArgs = [acceptorDict, event, sentArgs, foundWatch], taskChain = taskChain)
else: else:
# Handle the event immediately. # Handle the event immediately.

View File

@ -68,12 +68,18 @@ class BrowserObject:
def __nonzero__(self): def __nonzero__(self):
return True return True
def __call__(self, *args): def __call__(self, *args, **kw):
needsResponse = True
if 'needsResponse' in kw:
needsResponse = kw['needsResponse']
del kw['needsResponse']
if kw:
raise ArgumentError, 'Keyword arguments not supported'
try: try:
parentObj, attribName = self.__boundMethod parentObj, attribName = self.__boundMethod
if parentObj: if parentObj:
# Call it as a method. # Call it as a method.
needsResponse = True
if parentObj is self.__runner.dom and attribName == 'alert': if parentObj is self.__runner.dom and attribName == 'alert':
# As a special hack, we don't wait for the return # As a special hack, we don't wait for the return
# value from the alert() call, since this is a # value from the alert() call, since this is a
@ -98,7 +104,7 @@ class BrowserObject:
raise AttributeError raise AttributeError
else: else:
# Call it as a plain function. # Call it as a plain function.
result = self.__runner.scriptRequest('call', self, value = args) result = self.__runner.scriptRequest('call', self, value = args, needsResponse = needsResponse)
except EnvironmentError: except EnvironmentError:
# Some odd problem on the call. # Some odd problem on the call.
raise TypeError raise TypeError
@ -208,11 +214,17 @@ class MethodWrapper:
def __nonzero__(self): def __nonzero__(self):
return True return True
def __call__(self, *args): def __call__(self, *args, **kw):
needsResponse = True
if 'needsResponse' in kw:
needsResponse = kw['needsResponse']
del kw['needsResponse']
if kw:
raise ArgumentError, 'Keyword arguments not supported'
try: try:
parentObj, attribName = self.__boundMethod parentObj, attribName = self.__boundMethod
# Call it as a method. # Call it as a method.
needsResponse = True
if parentObj is self.__runner.dom and attribName == 'alert': if parentObj is self.__runner.dom and attribName == 'alert':
# As a special hack, we don't wait for the return # As a special hack, we don't wait for the return
# value from the alert() call, since this is a # value from the alert() call, since this is a

View File

@ -25,6 +25,7 @@ from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, HTTPClient, Thread, WindowProperties from pandac.PandaModules import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, HTTPClient, Thread, WindowProperties
from direct.stdpy import file from direct.stdpy import file
from direct.task.TaskManagerGlobal import taskMgr from direct.task.TaskManagerGlobal import taskMgr
from direct.showbase.MessengerGlobal import messenger
from direct.showbase import AppRunnerGlobal from direct.showbase import AppRunnerGlobal
# These imports are read by the C++ wrapper in p3dPythonRun.cxx. # These imports are read by the C++ wrapper in p3dPythonRun.cxx.
@ -98,6 +99,11 @@ class AppRunner(DirectObject):
if AppRunnerGlobal.appRunner is None: if AppRunnerGlobal.appRunner is None:
AppRunnerGlobal.appRunner = self AppRunnerGlobal.appRunner = self
# We use this messenger hook to dispatch this startIfReady()
# call back to the main thread.
self.accept('startIfReady', self.startIfReady)
def setSessionId(self, sessionId): def setSessionId(self, sessionId):
""" This message should come in at startup. """ """ This message should come in at startup. """
self.sessionId = sessionId self.sessionId = sessionId
@ -167,6 +173,9 @@ class AppRunner(DirectObject):
if self.gotWindow and self.gotP3DFilename: if self.gotWindow and self.gotP3DFilename:
self.started = True self.started = True
# Now we can ignore future calls to startIfReady().
self.ignore('startIfReady')
# Hang a hook so we know when the window is actually opened. # Hang a hook so we know when the window is actually opened.
self.acceptOnce('window-event', self.windowEvent) self.acceptOnce('window-event', self.windowEvent)
@ -265,7 +274,8 @@ class AppRunner(DirectObject):
self.gotP3DFilename = True self.gotP3DFilename = True
self.startIfReady() # Send this call to the main thread; don't call it directly.
messenger.send('startIfReady', taskChain = 'default')
def clearWindowPrc(self): def clearWindowPrc(self):
""" Clears the windowPrc file that was created in a previous """ Clears the windowPrc file that was created in a previous
@ -325,7 +335,9 @@ class AppRunner(DirectObject):
self.windowPrc = loadPrcFileData("setupWindow", data) self.windowPrc = loadPrcFileData("setupWindow", data)
self.gotWindow = True self.gotWindow = True
self.startIfReady()
# Send this call to the main thread; don't call it directly.
messenger.send('startIfReady', taskChain = 'default')
def setRequestFunc(self, func): def setRequestFunc(self, func):
""" This method is called by the plugin at startup to supply a """ This method is called by the plugin at startup to supply a
@ -468,4 +480,4 @@ if __name__ == '__main__':
except ArgumentError, e: except ArgumentError, e:
print e.args[0] print e.args[0]
sys.exit(1) sys.exit(1)
run() taskMgr.run()