Browse Source

async JavaScript

David Rose 16 years ago
parent
commit
aa8285f5a4

+ 92 - 55
direct/src/plugin/p3dPythonRun.cxx

@@ -168,11 +168,13 @@ run_python() {
   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[] = {
-    {"request_func", P3DPythonRun::st_request_func, METH_VARARGS,
-     "Send an asynchronous request to the plugin host"},
-    {NULL, NULL, 0, NULL}        /* Sentinel */
+    { "check_comm", P3DPythonRun::st_check_comm, METH_VARARGS,
+      "Poll for communications from the parent process" },
+    { "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);
   if (p3dpython == NULL) {
@@ -196,11 +198,31 @@ run_python() {
   Py_DECREF(request_func);
  
 
-  // Now add check_comm() as a task.
-  _check_comm_task = new GenericAsyncTask("check_comm", task_check_comm, this);
+  // Now add check_comm() as a task.  It can be a threaded task, but
+  // 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();
+  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);
 
+  Py_DECREF(check_comm);
+
   // Finally, get lost in taskMgr.run().
   PyObject *done = PyObject_CallMethod(_taskMgr, (char *)"run", (char *)"");
   if (done == NULL) {
@@ -216,11 +238,14 @@ run_python() {
 //     Function: P3DPythonRun::handle_command
 //       Access: Private
 //  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::
 handle_command(TiXmlDocument *doc) {
-  nout << "received: " << *doc << "\n" << flush;
   TiXmlElement *xcommand = doc->FirstChildElement("command");
   if (xcommand != NULL) {
     bool needs_response = false;
@@ -281,9 +306,15 @@ handle_command(TiXmlDocument *doc) {
         handle_pyobj_command(xcommand, needs_response, want_response_id);
 
       } else if (strcmp(cmd, "script_response") == 0) {
-        // Response from a script request.
-        assert(!needs_response);
-        nout << "Ignoring unexpected script_response\n";
+        // Response from a script request.  In this case, we just
+        // store it away instead of processing it immediately.
+
+        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) {
         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
 //       Access: Private
 //  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
 //               from the plugin host in the parent process.
 ////////////////////////////////////////////////////////////////////
 void P3DPythonRun::
 check_comm() {
-  nout << ":" << flush;
+  //  nout << ":" << flush;
   ACQUIRE_LOCK(_commands_lock);
   while (!_commands.empty()) {
     TiXmlDocument *doc = _commands.front();
     _commands.pop_front();
-    assert(_commands.size() < 10);
     RELEASE_LOCK(_commands_lock);
     handle_command(doc);
-    delete doc;
     ACQUIRE_LOCK(_commands_lock);
   }
+  RELEASE_LOCK(_commands_lock);
 
   if (!_program_continue) {
     // The low-level thread detected an error, for instance pipe
     // closed.  We should exit gracefully.
     terminate_session();
   }
-
-  RELEASE_LOCK(_commands_lock);
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DPythonRun::task_check_comm
+//     Function: P3DPythonRun::st_check_comm
 //       Access: Private, Static
-//  Description: This static function wrapper around check_comm is
-//               necessary to add the method function to the
-//               GenericAsyncTask object.
+//  Description: This is a static Python wrapper around py_check_comm,
+//               needed to add the function to a PythonTask.
 ////////////////////////////////////////////////////////////////////
-AsyncTask::DoneStatus P3DPythonRun::
-task_check_comm(GenericAsyncTask *task, void *user_data) {
-  P3DPythonRun *self = (P3DPythonRun *)user_data;
-  self->check_comm();
-  return AsyncTask::DS_cont;
+PyObject *P3DPythonRun::
+st_check_comm(PyObject *, PyObject *args) {
+  P3DPythonRun::_global_ptr->check_comm();
+  return Py_BuildValue("i", AsyncTask::DS_cont);
 }
 
 ////////////////////////////////////////////////////////////////////
@@ -611,54 +639,63 @@ task_check_comm(GenericAsyncTask *task, void *user_data) {
 ////////////////////////////////////////////////////////////////////
 TiXmlDocument *P3DPythonRun::
 wait_script_response(int response_id) {
-  nout << "waiting script_response " << response_id << "\n" << flush;
+  //  nout << "waiting script_response " << response_id << "\n" << flush;
   while (true) {
-    ACQUIRE_LOCK(_commands_lock);
-    
     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) {
       TiXmlDocument *doc = (*ci);
 
       TiXmlElement *xcommand = doc->FirstChildElement("command");
       if (xcommand != NULL) {
         const char *cmd = xcommand->Attribute("cmd");
-        if (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.
-              _commands.erase(ci);
-              RELEASE_LOCK(_commands_lock);
-              nout << "got script_response " << unique_id << "\n" << flush;
-              return doc;
-            }
-          }
-        }
+        if ((cmd != NULL && strcmp(cmd, "script_response") == 0) ||
+            xcommand->Attribute("want_response_id") != NULL) {
 
-        // It's not the response we're waiting for, but maybe we need
-        // to handle it anyway.
-        bool needs_response = false;
-        int want_response_id;
-        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;
+          // This is either a response, or it's a command that will
+          // want a response itself.  In either case we should handle
+          // it right away.  ("handling" a response means moving it to
+          // the _responses queue.)
           _commands.erase(ci);
           RELEASE_LOCK(_commands_lock);
           handle_command(doc);
-          delete doc;
           ACQUIRE_LOCK(_commands_lock);
           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) {
       terminate_session();
     }
-    
-    RELEASE_LOCK(_commands_lock);
 
 #ifdef _WIN32
     // 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);
 #endif  // _WIN32
 
-    nout << "." << flush;
+    //    nout << "." << flush;
 
     // It hasn't shown up yet.  Give the sub-thread a chance to
     // process the input and append it to the queue.

+ 11 - 4
direct/src/plugin/p3dPythonRun.h

@@ -25,9 +25,10 @@
 #include "p3d_lock.h"
 #include "handleStream.h"
 #include "p3dCInstance.h"
-#include "genericAsyncTask.h"
+#include "pythonTask.h"
 #include "pmap.h"
 #include "pdeque.h"
+#include "pmutex.h"
 #include "get_tinyxml.h"
 
 #include <Python.h>
@@ -75,7 +76,7 @@ private:
   void handle_script_response_command(TiXmlElement *xcommand);
 
   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);
 
   PyObject *py_request_func(PyObject *args);
@@ -118,7 +119,7 @@ private:
   PyObject *_browser_object_class;
   PyObject *_taskMgr;
 
-  PT(GenericAsyncTask) _check_comm_task;
+  PT(PythonTask) _check_comm_task;
 
   // This map keeps track of the PyObject pointers we have delivered
   // to the parent process.  We have to hold the reference count on
@@ -128,8 +129,14 @@ private:
   SentObjects _sent_objects;
   int _next_sent_id;
 
-  // The remaining members are manipulated by the read thread.
   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;
   
   // This has to be an actual OS LOCK instead of Panda's Mutex,

+ 1 - 0
direct/src/showbase/Messenger.py

@@ -311,6 +311,7 @@ class Messenger:
 
             if taskChain:
                 # 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)
             else:
                 # Handle the event immediately.

+ 17 - 5
direct/src/showutil/JavaScript.py

@@ -68,12 +68,18 @@ class BrowserObject:
     def __nonzero__(self):
         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:
             parentObj, attribName = self.__boundMethod
             if parentObj:
                 # Call it as a method.
-                needsResponse = True
                 if parentObj is self.__runner.dom and attribName == 'alert':
                     # As a special hack, we don't wait for the return
                     # value from the alert() call, since this is a
@@ -98,7 +104,7 @@ class BrowserObject:
                         raise AttributeError
             else:
                 # 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:
             # Some odd problem on the call.
             raise TypeError
@@ -208,11 +214,17 @@ class MethodWrapper:
     def __nonzero__(self):
         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:
             parentObj, attribName = self.__boundMethod
             # Call it as a method.
-            needsResponse = True
             if parentObj is self.__runner.dom and attribName == 'alert':
                 # As a special hack, we don't wait for the return
                 # value from the alert() call, since this is a

+ 15 - 3
direct/src/showutil/runp3d.py

@@ -25,6 +25,7 @@ from direct.showbase.DirectObject import DirectObject
 from pandac.PandaModules import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, HTTPClient, Thread, WindowProperties
 from direct.stdpy import file
 from direct.task.TaskManagerGlobal import taskMgr
+from direct.showbase.MessengerGlobal import messenger
 from direct.showbase import AppRunnerGlobal
 
 # These imports are read by the C++ wrapper in p3dPythonRun.cxx.
@@ -98,6 +99,11 @@ class AppRunner(DirectObject):
         if AppRunnerGlobal.appRunner is None:
             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):
         """ This message should come in at startup. """
         self.sessionId = sessionId
@@ -167,6 +173,9 @@ class AppRunner(DirectObject):
         if self.gotWindow and self.gotP3DFilename:
             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.
             self.acceptOnce('window-event', self.windowEvent)
 
@@ -265,7 +274,8 @@ class AppRunner(DirectObject):
 
         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):
         """ Clears the windowPrc file that was created in a previous
@@ -325,7 +335,9 @@ class AppRunner(DirectObject):
         self.windowPrc = loadPrcFileData("setupWindow", data)
 
         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):
         """ This method is called by the plugin at startup to supply a
@@ -468,4 +480,4 @@ if __name__ == '__main__':
     except ArgumentError, e:
         print e.args[0]
         sys.exit(1)
-    run()
+    taskMgr.run()