Browse Source

more work on outbound scripting

David Rose 16 years ago
parent
commit
59210bf718

+ 5 - 5
direct/src/plugin/p3dInstance.cxx

@@ -173,7 +173,7 @@ set_wparams(const P3DWindowParams &wparams) {
 //               of the instance, to be used by JavaScript code in the
 //               browser to control this program.
 ////////////////////////////////////////////////////////////////////
-P3DObject *P3DInstance::
+P3D_object *P3DInstance::
 get_panda_script_object() const {
   assert(_session != NULL);
   nout << "Called P3DInstance::get_panda_script_object()\n";
@@ -188,13 +188,13 @@ get_panda_script_object() const {
   TiXmlDocument *response = _session->command_and_response(doc);
   nout << "response pointer: " << response << "\n" << flush;
 
-  P3DObject *result = NULL;
+  P3D_object *result = NULL;
   if (response != NULL) {
     TiXmlElement *xresponse = response->FirstChildElement("response");
     if (xresponse != NULL) {
       TiXmlElement *xvalue = xresponse->FirstChildElement("value");
       if (xvalue != NULL) {
-        result = _session->xml_to_object(xvalue);
+        result = _session->xml_to_p3dobj(xvalue);
       }
     }
     delete response;
@@ -464,7 +464,7 @@ send_browser_script_object() {
   xcommand->SetAttribute("cmd", "pyobj");
   xcommand->SetAttribute("op", "set_browser_script_object");
   if (_browser_script_object != NULL) {
-    xcommand->LinkEndChild(_session->object_to_xml(_browser_script_object));
+    xcommand->LinkEndChild(_session->p3dobj_to_xml(_browser_script_object));
   }
   
   doc->LinkEndChild(decl);
@@ -534,7 +534,7 @@ handle_script_request(P3D_request *request) {
       doc->LinkEndChild(decl);
       doc->LinkEndChild(xcommand);
       if (value != NULL) {
-        xcommand->LinkEndChild(_session->object_to_xml(value));
+        xcommand->LinkEndChild(_session->p3dobj_to_xml(value));
         P3D_OBJECT_FINISH(value);
       }
       

+ 1 - 1
direct/src/plugin/p3dInstance.h

@@ -46,7 +46,7 @@ public:
   void set_wparams(const P3DWindowParams &wparams);
   inline const P3DWindowParams &get_wparams() const;
 
-  P3DObject *get_panda_script_object() const;
+  P3D_object *get_panda_script_object() const;
   void set_browser_script_object(P3D_object *object);
 
   bool has_request();

+ 3 - 3
direct/src/plugin/p3dPythonObject.cxx

@@ -194,7 +194,7 @@ call(const string &method_name, P3D_object *params[], int num_params) const {
   }
 
   for (int i = 0; i < num_params; ++i) {
-    TiXmlElement *xparams = _session->object_to_xml(params[i]);
+    TiXmlElement *xparams = _session->p3dobj_to_xml(params[i]);
     xcommand->LinkEndChild(xparams);
 
     // Now we're done with the params object passed in, we can delete
@@ -207,13 +207,13 @@ call(const string &method_name, P3D_object *params[], int num_params) const {
   TiXmlDocument *response = _session->command_and_response(doc);
   nout << "call response pointer: " << response << "\n" << flush;
 
-  P3DObject *result = NULL;
+  P3D_object *result = NULL;
   if (response != NULL) {
     TiXmlElement *xresponse = response->FirstChildElement("response");
     if (xresponse != NULL) {
       TiXmlElement *xvalue = xresponse->FirstChildElement("value");
       if (xvalue != NULL) {
-        result = _session->xml_to_object(xvalue);
+        result = _session->xml_to_p3dobj(xvalue);
       }
     }
     delete response;

+ 117 - 37
direct/src/plugin/p3dPythonRun.cxx

@@ -172,7 +172,7 @@ run_python() {
  
 
   // Now add check_comm() as a task.
-  _check_comm_task = new GenericAsyncTask("check_comm", st_check_comm, this);
+  _check_comm_task = new GenericAsyncTask("check_comm", task_check_comm, this);
   AsyncTaskManager *task_mgr = AsyncTaskManager::get_global_ptr();
   task_mgr->add(_check_comm_task);
 
@@ -244,7 +244,7 @@ handle_command(TiXmlDocument *doc) {
       } else if (strcmp(cmd, "script_response") == 0) {
         // Response from a script request.
         assert(!needs_response);
-        handle_script_response_command(xcommand);
+        nout << "Ignoring unexpected script_response\n";
         
       } else {
         nout << "Unhandled command " << cmd << "\n";
@@ -408,41 +408,16 @@ handle_pyobj_command(TiXmlElement *xcommand, bool needs_response,
   }
 }
 
-////////////////////////////////////////////////////////////////////
-//     Function: P3DPythonRun::handle_script_response_command
-//       Access: Private
-//  Description: Handles the script_response command, a response from
-//               the browser to a previous script request from this
-//               process.
-////////////////////////////////////////////////////////////////////
-void P3DPythonRun::
-handle_script_response_command(TiXmlElement *xcommand) {
-  int unique_id;
-  if (xcommand->QueryIntAttribute("unique_id", &unique_id) == TIXML_SUCCESS) {
-    PyObject *value = NULL;
-    TiXmlElement *xvalue = xcommand->FirstChildElement("value");
-    if (xvalue != NULL) {
-      value = xml_to_pyobj(xvalue);
-    } else {
-      value = Py_None;
-      Py_INCREF(value);
-    }
-    PyObject *result = PyObject_CallMethod
-      (_runner, (char *)"scriptResponse", (char *)"iO", unique_id, value);
-    Py_DECREF(value);
-  }
-}
-
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DPythonRun::check_comm
 //       Access: Private
-//  Description: This method is added to the Python task manager (via
-//               py_check_comm, below) so that it gets a call every
+//  Description: This method is added to the task manager (via
+//               task_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.
 ////////////////////////////////////////////////////////////////////
-AsyncTask::DoneStatus P3DPythonRun::
-check_comm(GenericAsyncTask *task) {
+void P3DPythonRun::
+check_comm() {
   ACQUIRE_LOCK(_commands_lock);
   while (!_commands.empty()) {
     TiXmlDocument *doc = _commands.front();
@@ -461,21 +436,82 @@ check_comm(GenericAsyncTask *task) {
   }
 
   RELEASE_LOCK(_commands_lock);
-
-  return AsyncTask::DS_cont;
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DPythonRun::st_check_comm
+//     Function: P3DPythonRun::task_check_comm
 //       Access: Private, Static
 //  Description: This static function wrapper around check_comm is
 //               necessary to add the method function to the
 //               GenericAsyncTask object.
 ////////////////////////////////////////////////////////////////////
 AsyncTask::DoneStatus P3DPythonRun::
-st_check_comm(GenericAsyncTask *task, void *user_data) {
+task_check_comm(GenericAsyncTask *task, void *user_data) {
   P3DPythonRun *self = (P3DPythonRun *)user_data;
-  return self->check_comm(task);
+  self->check_comm();
+  return AsyncTask::DS_cont;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DPythonRun::wait_script_response
+//       Access: Private
+//  Description: This method is similar to check_comm(), above, but
+//               instead of handling all events, it waits for a
+//               specific script_response ID to come back from the
+//               browser, and leaves all other events in the queue.
+////////////////////////////////////////////////////////////////////
+TiXmlDocument *P3DPythonRun::
+wait_script_response(int response_id) {
+  while (true) {
+    ACQUIRE_LOCK(_commands_lock);
+    
+    Commands::iterator ci;
+    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);
+              return doc;
+            }
+          }
+        }
+
+        // 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.
+          _commands.erase(ci);
+          RELEASE_LOCK(_commands_lock);
+          handle_command(doc);
+          delete doc;
+          ACQUIRE_LOCK(_commands_lock);
+          break;
+        }
+      }
+    }
+    
+    if (!_program_continue) {
+      terminate_session();
+    }
+    
+    RELEASE_LOCK(_commands_lock);
+
+    // It hasn't shown up yet.  Give the sub-thread a chance to
+    // process the input and append it to the queue.
+    Thread::force_yield();
+  }
 }
 
 ////////////////////////////////////////////////////////////////////
@@ -494,6 +530,38 @@ py_request_func(PyObject *args) {
     return NULL;
   }
 
+  if (strcmp(request_type, "wait_script_response") == 0) {
+    // This is a special case.  Instead of generating a new request,
+    // this means to wait for a particular script_response to come in
+    // on the wire.
+    int response_id;
+    if (!PyArg_ParseTuple(extra_args, "i", &response_id)) {
+      Py_DECREF(extra_args);
+      return NULL;
+    }
+
+    nout << "Waiting for script_response " << response_id << "\n";
+    TiXmlDocument *doc = wait_script_response(response_id);
+    nout << "got: " << *doc << "\n";
+    TiXmlElement *xcommand = doc->FirstChildElement("command");
+    assert(xcommand != NULL);
+    TiXmlElement *xvalue = xcommand->FirstChildElement("value");
+
+    PyObject *value = NULL;
+    if (xvalue != NULL) {
+      nout << "Converting xvalue: " << *xvalue << "\n";
+      value = xml_to_pyobj(xvalue);
+    } else {
+      value = Py_None;
+      Py_INCREF(value);
+    }
+    nout << "Got script_response " << response_id << ", xvalue = " << xvalue << "\n";
+
+    delete doc;
+    Py_DECREF(extra_args);
+    return value;
+  }
+
   TiXmlDocument doc;
   TiXmlDeclaration *decl = new TiXmlDeclaration("1.0", "utf-8", "");
   TiXmlElement *xrequest = new TiXmlElement("request");
@@ -506,6 +574,7 @@ py_request_func(PyObject *args) {
     // A general notification to be sent directly to the instance.
     const char *message;
     if (!PyArg_ParseTuple(extra_args, "s", &message)) {
+      Py_DECREF(extra_args);
       return NULL;
     }
 
@@ -522,8 +591,10 @@ py_request_func(PyObject *args) {
     int unique_id;
     if (!PyArg_ParseTuple(extra_args, "sOsOi", 
                           &operation, &object, &property_name, &value, &unique_id)) {
+      Py_DECREF(extra_args);
       return NULL;
     }
+
     xrequest->SetAttribute("operation", operation);
     xrequest->SetAttribute("property_name", property_name);
     xrequest->SetAttribute("unique_id", unique_id);
@@ -542,6 +613,7 @@ py_request_func(PyObject *args) {
     const char *expression;
     int unique_id;
     if (!PyArg_ParseTuple(extra_args, "si", &expression, &unique_id)) {
+      Py_DECREF(extra_args);
       return NULL;
     }
 
@@ -554,9 +626,11 @@ py_request_func(PyObject *args) {
   } else {
     string message = string("Unsupported request type: ") + string(request_type);
     PyErr_SetString(PyExc_ValueError, message.c_str());
+    Py_DECREF(extra_args);
     return NULL;
   }
 
+  Py_DECREF(extra_args);
   return Py_BuildValue("");
 }
 
@@ -780,6 +854,11 @@ terminate_session() {
   }
   Py_DECREF(result);
   nout << "done calling stop()\n";
+
+  // The task manager is cleaned up.  Let's exit immediately here,
+  // rather than returning all the way up.  This just makes it easier
+  // when we call terminate_session() from a deeply-nested loop.
+  exit(0);
 }
 
 ////////////////////////////////////////////////////////////////////
@@ -921,7 +1000,8 @@ xml_to_pyobj(TiXmlElement *xvalue) {
     int object_id;
     if (xvalue->QueryIntAttribute("object_id", &object_id) == TIXML_SUCCESS) {
       // Construct a new BrowserObject wrapper around this object.
-      return PyObject_CallFunction(_browser_object_class, (char *)"i", object_id);
+      return PyObject_CallFunction(_browser_object_class, (char *)"Oi", 
+                                   _runner, object_id);
     }
 
   } else if (strcmp(type, "python") == 0) {

+ 3 - 2
direct/src/plugin/p3dPythonRun.h

@@ -74,8 +74,9 @@ private:
                             int want_response_id);
   void handle_script_response_command(TiXmlElement *xcommand);
 
-  AsyncTask::DoneStatus check_comm(GenericAsyncTask *task);
-  static AsyncTask::DoneStatus st_check_comm(GenericAsyncTask *task, void *user_data);
+  void check_comm();
+  static AsyncTask::DoneStatus task_check_comm(GenericAsyncTask *task, void *user_data);
+  TiXmlDocument *wait_script_response(int response_id);
 
   PyObject *py_request_func(PyObject *args);
   static PyObject *st_request_func(PyObject *, PyObject *args);

+ 20 - 7
direct/src/plugin/p3dSession.cxx

@@ -312,14 +312,14 @@ command_and_response(TiXmlDocument *command) {
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DSession::xml_to_object
+//     Function: P3DSession::xml_to_p3dobj
 //       Access: Public
 //  Description: Converts the XML representation of the particular
-//               object value into a corresponding P3DObject object.
+//               object value into a corresponding P3D_object.
 //               Returns the newly-allocated object.
 ////////////////////////////////////////////////////////////////////
-P3DObject *P3DSession::
-xml_to_object(const TiXmlElement *xvalue) {
+P3D_object *P3DSession::
+xml_to_p3dobj(const TiXmlElement *xvalue) {
   const char *type = xvalue->Attribute("type");
   if (strcmp(type, "none") == 0) {
     return new P3DNoneObject;
@@ -350,6 +350,15 @@ xml_to_object(const TiXmlElement *xvalue) {
       return new P3DStringObject(*value);
     }
 
+  } else if (strcmp(type, "browser") == 0) {
+    int object_id;
+    if (xvalue->QueryIntAttribute("object_id", &object_id) == TIXML_SUCCESS) {
+      P3D_object *obj = (P3D_object *)object_id;
+      nout << "Found object " << obj << "\n" << flush;
+      nout << "  formatted is " << *obj << "\n" << flush;
+      return P3D_OBJECT_COPY(obj);
+    }
+
   } else if (strcmp(type, "python") == 0) {
     int object_id;
     if (xvalue->QueryIntAttribute("object_id", &object_id) == TIXML_SUCCESS) {
@@ -362,14 +371,14 @@ xml_to_object(const TiXmlElement *xvalue) {
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DSession::object_to_xml
+//     Function: P3DSession::p3dobj_to_xml
 //       Access: Public
 //  Description: Allocates and returns a new XML structure
 //               corresponding to the indicated value.  The supplied
 //               P3DObject passed in is *not* deleted.
 ////////////////////////////////////////////////////////////////////
 TiXmlElement *P3DSession::
-object_to_xml(const P3D_object *obj) {
+p3dobj_to_xml(const P3D_object *obj) {
   TiXmlElement *xvalue = new TiXmlElement("value");
 
   switch (P3D_OBJECT_GET_TYPE(obj)) {
@@ -681,7 +690,11 @@ rt_make_p3d_request(TiXmlElement *xrequest) {
       xrequest->Attribute("unique_id", &unique_id);
 
       if (operation != NULL && xobject != NULL) {
-        P3D_object *object = xml_to_object(xobject);
+        nout << "xobject = " << *xobject << "\n" << flush;
+        P3D_object *object = xml_to_p3dobj(xobject);
+        nout << "converted to " << object << "\n" << flush;
+        nout << "object = " << *object << "\n" << flush;
+        nout << "operation = " << *operation << "\n" << flush;
         if (strcmp(operation, "get_property") == 0 && property_name != NULL) {
           request = new P3D_request;
           request->_request_type = P3D_RT_script;

+ 2 - 2
direct/src/plugin/p3dSession.h

@@ -49,8 +49,8 @@ public:
 
   void send_command(TiXmlDocument *command);
   TiXmlDocument *command_and_response(TiXmlDocument *command);
-  P3DObject *xml_to_object(const TiXmlElement *xvalue);
-  TiXmlElement *object_to_xml(const P3D_object *obj);
+  P3D_object *xml_to_p3dobj(const TiXmlElement *xvalue);
+  TiXmlElement *p3dobj_to_xml(const P3D_object *obj);
 
 private:
   void install_progress(P3DPackage *package, double progress);

+ 1 - 1
direct/src/plugin_npapi/ppBrowserObject.cxx

@@ -128,7 +128,7 @@ get_property(const string &property) const {
     return NULL;
   }
 
-  P3D_object *object = _instance->variant_to_object(&result);
+  P3D_object *object = _instance->variant_to_p3dobj(&result);
   browser->releasevariantvalue(&result);
   return object;
 }

+ 5 - 37
direct/src/plugin_npapi/ppInstance.cxx

@@ -499,13 +499,13 @@ get_panda_script_object() {
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: PPInstance::object_to_variant
+//     Function: PPInstance::p3dobj_to_variant
 //       Access: Private
 //  Description: Converts the indicated P3D_object to the equivalent
 //               NPVariant, and stores it in result.
 ////////////////////////////////////////////////////////////////////
 void PPInstance::
-object_to_variant(NPVariant *result, const P3D_object *object) {
+p3dobj_to_variant(NPVariant *result, const P3D_object *object) {
   switch (P3D_OBJECT_GET_TYPE(object)) {
   case P3D_OT_none:
     VOID_TO_NPVARIANT(*result);
@@ -542,7 +542,7 @@ object_to_variant(NPVariant *result, const P3D_object *object) {
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: PPInstance::variant_to_object
+//     Function: PPInstance::variant_to_p3dobj
 //       Access: Private
 //  Description: Converts the indicated NPVariant to the equivalent
 //               P3D_object, and returns it (newly-allocated).  The
@@ -550,7 +550,7 @@ object_to_variant(NPVariant *result, const P3D_object *object) {
 //               later.
 ////////////////////////////////////////////////////////////////////
 P3D_object *PPInstance::
-variant_to_object(const NPVariant *variant) {
+variant_to_p3dobj(const NPVariant *variant) {
   if (NPVARIANT_IS_VOID(*variant) ||
       NPVARIANT_IS_NULL(*variant)) {
     return P3D_new_none_object();
@@ -564,8 +564,7 @@ variant_to_object(const NPVariant *variant) {
     NPString str = NPVARIANT_TO_STRING(*variant);
     return P3D_new_string_object(str.utf8characters, str.utf8length);
   } else if (NPVARIANT_IS_OBJECT(*variant)) {
-    // TODO.
-    return P3D_new_none_object();
+    return new PPBrowserObject(this, NPVARIANT_TO_OBJECT(*variant));
   }
 
   // Hmm, none of the above?
@@ -826,37 +825,6 @@ show_np_variant(const NPVariant &result) {
   }
 }
 
-////////////////////////////////////////////////////////////////////
-//     Function: PPInstance::np_variant_to_object
-//       Access: Private
-//  Description: Returns a freshly-allocated P3D_object corresponding
-//               to the indicated NPVariant.
-////////////////////////////////////////////////////////////////////
-P3D_object *PPInstance::
-np_variant_to_object(const NPVariant &result) {
-  if (NPVARIANT_IS_NULL(result)) {
-    return NULL;
-  } else if (NPVARIANT_IS_VOID(result)) {
-    return P3D_new_none_object();
-  } else if (NPVARIANT_IS_BOOLEAN(result)) {
-    return P3D_new_bool_object(NPVARIANT_TO_BOOLEAN(result));
-  } else if (NPVARIANT_IS_INT32(result)) {
-    return P3D_new_int_object(NPVARIANT_TO_INT32(result));
-  } else if (NPVARIANT_IS_DOUBLE(result)) {
-    return P3D_new_float_object(NPVARIANT_TO_DOUBLE(result));
-  } else if (NPVARIANT_IS_STRING(result)) {
-    NPString str = NPVARIANT_TO_STRING(result);
-    return P3D_new_string_object(str.utf8characters, str.utf8length);
-  } else if (NPVARIANT_IS_OBJECT(result)) {
-    // TODO?
-    return P3D_new_none_object();
-    // NPVARIANT_TO_OBJECT(result);
-  }
-
-  // Huh, what is this?
-  return NULL;
-}
-
 
 #ifdef _WIN32
 ////////////////////////////////////////////////////////////////////

+ 2 - 3
direct/src/plugin_npapi/ppInstance.h

@@ -52,8 +52,8 @@ public:
 
   NPObject *get_panda_script_object();
 
-  void object_to_variant(NPVariant *result, const P3D_object *object);
-  P3D_object *variant_to_object(const NPVariant *variant);
+  void p3dobj_to_variant(NPVariant *result, const P3D_object *object);
+  P3D_object *variant_to_p3dobj(const NPVariant *variant);
 
 private:
   bool read_contents_file(const string &filename);
@@ -65,7 +65,6 @@ private:
   void send_window();
 
   void show_np_variant(const NPVariant &result);
-  P3D_object *np_variant_to_object(const NPVariant &result);
 
 #ifdef _WIN32
   static LONG 

+ 2 - 2
direct/src/plugin_npapi/ppPandaObject.cxx

@@ -175,7 +175,7 @@ get_property(NPIdentifier name, NPVariant *result) {
   }
 
   // We have the property, and its value is stored in value.
-  _instance->object_to_variant(result, value);
+  _instance->p3dobj_to_variant(result, value);
   P3D_OBJECT_FINISH(value);
   return true;
 }
@@ -195,7 +195,7 @@ set_property(NPIdentifier name, const NPVariant *value) {
     return false;
   }
 
-  P3D_object *object = _instance->variant_to_object(value);
+  P3D_object *object = _instance->variant_to_p3dobj(value);
   bool result = P3D_OBJECT_SET_PROPERTY(_p3d_object, property_name.c_str(), object);
   return result;
 }

+ 53 - 11
direct/src/showutil/runp3d.py

@@ -22,7 +22,7 @@ See pack3d.py for a script that generates these p3d files.
 import sys
 from direct.showbase import VFSImporter
 from direct.showbase.DirectObject import DirectObject
-from pandac.PandaModules import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, HTTPClient
+from pandac.PandaModules import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, HTTPClient, Thread
 from direct.stdpy import file
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.showbase import AppRunnerGlobal
@@ -155,8 +155,9 @@ class AppRunner(DirectObject):
         """ Replaces self.window with the browser's toplevel DOM
         object, for controlling the JavaScript and the document in the
         same page with the Panda3D plugin. """
-        print "setBrowserScriptObject(%s)" % (window)
+
         self.window = window
+        print "setBrowserScriptObject(%s)" % (window)
 
     def setP3DFilename(self, p3dFilename, tokens = [],
                        instanceId = None):
@@ -261,7 +262,8 @@ class AppRunner(DirectObject):
         self.requestFunc = func
 
     def sendRequest(self, request, *args):
-        self.requestFunc(self.instanceId, request, args)
+        assert self.requestFunc
+        return self.requestFunc(self.instanceId, request, args)
 
     def windowEvent(self, win):
         print "Got window event in runp3d"
@@ -271,7 +273,9 @@ class AppRunner(DirectObject):
     def scriptRequest(self, operation, object, propertyName = None,
                       value = None):
         """ Issues a new script request to the browser.  This queries
-        or modifies one of the browser's DOM properties.
+        or modifies one of the browser's DOM properties.  This method
+        blocks until the return value is received from the browser,
+        and then it returns that value.
         
         operation may be one of [ 'get_property', 'set_property',
         'call', 'evaluate' ].
@@ -291,10 +295,10 @@ class AppRunner(DirectObject):
         self.sendRequest('script', operation, object,
                          propertyName, value, uniqueId);
 
-    def scriptResponse(self, uniqueId, value):
-        """ Called by the browser in response to a scriptRequest,
-        above. """
-        print "Got scriptResponse: %s, %s" % (uniqueId, value)
+        # Now wait for the response to come in.
+        result = self.sendRequest('wait_script_response', uniqueId)
+        print "result for %s.%s = %s" % (object, propertyName, result,)
+        return result
 
     def parseSysArgs(self):
         """ Converts sys.argv into (p3dFilename, tokens). """
@@ -333,9 +337,47 @@ class BrowserObject:
     actually exists in the plugin host's namespace, e.g. a JavaScript
     or DOM object. """
 
-    def __init__(self, objectId):
-        self.__objectId = objectId
-        
+    def __init__(self, runner, objectId):
+        self.__dict__['_BrowserObject__runner'] = runner
+        self.__dict__['_BrowserObject__objectId'] = objectId
+
+    def __str__(self):
+        return "BrowserObject(%s)" % (self.__objectId)
+
+    def __nonzero__(self):
+        return True
+
+    def __getattr__(self, name):
+        """ Remaps attempts to query an attribute into the appropriate
+        calls to query the actual browser object under the hood.  """
+
+        print "__getattr_(self, %s)" % (name)
+        print "runner = %s" % (self.__runner)
+        value = self.__runner.scriptRequest('get_property', self,
+                                            propertyName = name)
+        return value
+        # raise AttributeError(name)
+
+    def __setattr__(self, name, value):
+        if name in self.__dict__:
+            self.__dict__[name] = value
+            return
+
+        value = self.__runner.scriptRequest('set_property', self,
+                                            propertyName = name,
+                                            value = value)
+        if not value:
+            raise AttributeError(name)
+
+    def __delattr__(self, name):
+        if name in self.__dict__:
+            del self.__dict__[name]
+            return
+
+        value = self.__runner.scriptRequest('del_property', self,
+                                            propertyName = name)
+        if not value:
+            raise AttributeError(name)
 
 if __name__ == '__main__':
     runner = AppRunner()