Browse Source

contents.xml max_age; internal cache busting

David Rose 15 years ago
parent
commit
0b80f17c68

+ 18 - 4
direct/src/p3d/Packager.py

@@ -1864,6 +1864,10 @@ class Packager:
         self.host = PandaSystem.getPackageHostUrl()
         self.addHost(self.host)
 
+        # The maximum amount of time a client should cache the
+        # contents.xml before re-querying the server, in seconds.
+        self.maxAge = 0
+
         # A search list for previously-built local packages.
 
         # We use a bit of caution to read the Filenames out of the
@@ -1918,10 +1922,10 @@ class Packager:
         # client and is therefore readily available to any hacker.
         # Not only is this feature useless, but using it also
         # increases the size of your patchfiles, since encrypted files
-        # don't patch as tightly as unencrypted files.  But it's here
-        # if you really want it.
-        self.encryptExtensions = ['ptf', 'dna', 'txt', 'dc']
-        self.encryptFiles = []
+        # can't really be patched.  But it's here if you really want
+        # it. ** Note: Actually, this isn't implemented yet.
+        #self.encryptExtensions = []
+        #self.encryptFiles = []
 
         # This is the list of DC import suffixes that should be
         # available to the client.  Other suffixes, like AI and UD,
@@ -2220,6 +2224,8 @@ class Packager:
         # Set up the namespace dictionary for exec.
         globals = {}
         globals['__name__'] = packageDef.getBasenameWoExtension()
+        globals['__dir__'] = Filename(packageDef.getDirname()).toOsSpecific()
+        globals['packageDef'] = packageDef
 
         globals['platform'] = self.platform
         globals['packager'] = self
@@ -3156,6 +3162,7 @@ class Packager:
         # sure that our own host at least is added to the map.
         self.addHost(self.host)
 
+        self.maxAge = 0
         self.contents = {}
         self.contentsChanged = False
 
@@ -3171,6 +3178,10 @@ class Packager:
 
         xcontents = doc.FirstChildElement('contents')
         if xcontents:
+            maxAge = xcontents.Attribute('max_age')
+            if maxAge:
+                self.maxAge = int(maxAge)
+                
             xhost = xcontents.FirstChildElement('host')
             if xhost:
                 he = self.HostEntry()
@@ -3199,6 +3210,9 @@ class Packager:
         doc.InsertEndChild(decl)
 
         xcontents = TiXmlElement('contents')
+        if self.maxAge:
+            xcontents.SetAttribute('max_age', str(self.maxAge))
+            
         if self.host:
             he = self.hosts.get(self.host, None)
             if he:

+ 11 - 0
direct/src/plugin/fileSpec.I

@@ -67,6 +67,17 @@ get_timestamp() const {
   return _timestamp;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: FileSpec::has_hash
+//       Access: Public
+//  Description: Returns true if we have successfully read a hash
+//               value, false otherwise.
+////////////////////////////////////////////////////////////////////
+inline bool FileSpec::
+has_hash() const {
+  return _got_hash;
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: FileSpec::get_actual_file
 //       Access: Public

+ 26 - 0
direct/src/plugin/fileSpec.cxx

@@ -123,6 +123,30 @@ load_xml(TiXmlElement *xelement) {
   }
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: FileSpec::store_xml
+//       Access: Public
+//  Description: Stores the data to the indicated XML file.
+////////////////////////////////////////////////////////////////////
+void FileSpec::
+store_xml(TiXmlElement *xelement) {
+  if (!_filename.empty()) {
+    xelement->SetAttribute("filename", _filename);
+  }
+  if (_size != 0) {
+    xelement->SetAttribute("size", _size);
+  }
+  if (_timestamp != 0) {
+    xelement->SetAttribute("timestamp", _timestamp);
+  }
+  if (_got_hash) {
+    char hash[hash_size * 2 + 1];
+    encode_hex(hash, _hash, hash_size);
+    hash[hash_size * 2] = '\0';
+    xelement->SetAttribute("hash", hash);
+  }
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: FileSpec::quick_verify
 //       Access: Public
@@ -272,6 +296,7 @@ check_hash(const string &pathname) const {
 bool FileSpec::
 read_hash(const string &pathname) {
   memset(_hash, 0, hash_size);
+  _got_hash = false;
 
   ifstream stream(pathname.c_str(), ios::in | ios::binary);
   if (!stream) {
@@ -294,6 +319,7 @@ read_hash(const string &pathname) {
   }
 
   MD5_Final(_hash, &ctx);
+  _got_hash = true;
 
   return true;
 }

+ 2 - 0
direct/src/plugin/fileSpec.h

@@ -34,12 +34,14 @@ public:
   ~FileSpec();
 
   void load_xml(TiXmlElement *xelement);
+  void store_xml(TiXmlElement *xelement);
 
   inline const string &get_filename() const;
   inline void set_filename(const string &filename);
   inline string get_pathname(const string &package_dir) const;
   inline size_t get_size() const;
   inline time_t get_timestamp() const;
+  inline bool has_hash() const;
   
   bool quick_verify(const string &package_dir);
   bool quick_verify_pathname(const string &pathname);

+ 2 - 1
direct/src/plugin/p3dHost.I

@@ -107,7 +107,8 @@ get_contents_seq() const {
 //     Function: P3DHost::check_contents_hash
 //       Access: Public
 //  Description: Returns true if the indicated pathname has the same
-//               md5 hash as the contents.xml file, false otherwise.
+//               md5 hash as the contents.xml file (as provided by the
+//               server), false otherwise.
 ////////////////////////////////////////////////////////////////////
 inline bool P3DHost::
 check_contents_hash(const string &pathname) const {

+ 135 - 24
direct/src/plugin/p3dHost.cxx

@@ -40,6 +40,7 @@ P3DHost(const string &host_url) :
   _descriptive_name = _host_url;
 
   _xcontents = NULL;
+  _contents_expiration = 0;
   _contents_seq = 0;
 }
 
@@ -106,6 +107,25 @@ get_alt_host(const string &alt_host) {
   return this;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::has_current_contents_file
+//       Access: Public
+//  Description: Returns true if a contents.xml file has been
+//               successfully read for this host and is still current,
+//               false otherwise.
+////////////////////////////////////////////////////////////////////
+bool P3DHost::
+has_current_contents_file(P3DInstanceManager *inst_mgr) const {
+  if (!inst_mgr->get_verify_contents()) {
+    // If we're not asking to verify contents, then contents.xml files
+    // never expire.
+    return has_contents_file();
+  }
+
+  time_t now = time(NULL);
+  return now < _contents_expiration && (_xcontents != NULL);
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DHost::read_contents_file
 //       Access: Public
@@ -122,21 +142,21 @@ read_contents_file() {
   }
 
   string standard_filename = _host_dir + "/contents.xml";
-  return read_contents_file(standard_filename);
+  return read_contents_file(standard_filename, false);
 }
 
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DHost::read_contents_file
 //       Access: Public
 //  Description: Reads the contents.xml file in the indicated
-//               filename.  On success, copies the contents.xml file
+//               filename.  On success, writes the contents.xml file
 //               into the standard location (if it's not there
 //               already).
 //
 //               Returns true on success, false on failure.
 ////////////////////////////////////////////////////////////////////
 bool P3DHost::
-read_contents_file(const string &contents_filename) {
+read_contents_file(const string &contents_filename, bool fresh_download) {
   TiXmlDocument doc(contents_filename.c_str());
   if (!doc.LoadFile()) {
     return false;
@@ -152,7 +172,50 @@ read_contents_file(const string &contents_filename) {
   }
   _xcontents = (TiXmlElement *)xcontents->Clone();
   ++_contents_seq;
-  _contents_spec.read_hash(contents_filename);
+  _contents_spec = FileSpec();
+
+  int max_age = P3D_CONTENTS_DEFAULT_MAX_AGE;
+  xcontents->Attribute("max_age", &max_age);
+
+  // Get the latest possible expiration time, based on the max_age
+  // indication.  Any expiration time later than this is in error.
+  time_t now = time(NULL);
+  _contents_expiration = now + (time_t)max_age;
+
+  if (fresh_download) {
+    _contents_spec.read_hash(contents_filename);
+
+    // Update the XML with the new download information.
+    TiXmlElement *xorig = xcontents->FirstChildElement("orig");
+    while (xorig != NULL) {
+      xcontents->RemoveChild(xorig);
+      xorig = xcontents->FirstChildElement("orig");
+    }
+
+    xorig = new TiXmlElement("orig");
+    xcontents->LinkEndChild(xorig);
+    _contents_spec.store_xml(xorig);
+
+    xorig->SetAttribute("expiration", (int)_contents_expiration);
+
+  } else {
+    // Read the download hash and expiration time from the XML.
+    int expiration = 0;
+    TiXmlElement *xorig = xcontents->FirstChildElement("orig");
+    if (xorig != NULL) {
+      _contents_spec.load_xml(xorig);
+      xorig->Attribute("expiration", &expiration);
+    }
+    if (!_contents_spec.has_hash()) {
+      _contents_spec.read_hash(contents_filename);
+    }
+
+    _contents_expiration = min(_contents_expiration, (time_t)expiration);
+  }
+
+  nout << "read contents.xml, max_age = " << max_age
+       << ", expires in " << max(_contents_expiration, now) - now
+       << " s\n";
 
   TiXmlElement *xhost = _xcontents->FirstChildElement("host");
   if (xhost != NULL) {
@@ -194,10 +257,16 @@ read_contents_file(const string &contents_filename) {
   mkdir_complete(_host_dir, nout);
 
   string standard_filename = _host_dir + "/contents.xml";
-  if (standardize_filename(standard_filename) != 
-      standardize_filename(contents_filename)) {
-    if (!copy_file(contents_filename, standard_filename)) {
-      nout << "Couldn't copy to " << standard_filename << "\n";
+  if (fresh_download) {
+    if (!save_xml_file(&doc, standard_filename)) {
+      nout << "Couldn't save to " << standard_filename << "\n";
+    }
+  } else {
+    if (standardize_filename(standard_filename) != 
+        standardize_filename(contents_filename)) {
+      if (!copy_file(contents_filename, standard_filename)) {
+        nout << "Couldn't copy to " << standard_filename << "\n";
+      }
     }
   }
 
@@ -208,14 +277,13 @@ read_contents_file(const string &contents_filename) {
     // iteration.
     string top_filename = inst_mgr->get_root_dir() + "/contents.xml";
     if (standardize_filename(top_filename) != 
-        standardize_filename(contents_filename)) {
-      if (!copy_file(contents_filename, top_filename)) {
+        standardize_filename(standard_filename)) {
+      if (!copy_file(standard_filename, top_filename)) {
         nout << "Couldn't copy to " << top_filename << "\n";
       }
     }
   }
 
-
   return true;
 }
 
@@ -288,25 +356,30 @@ get_package(const string &package_name, const string &package_version,
 
   PackageMap &package_map = _packages[alt_host];
 
+  P3DPackage *package = NULL;
+
   string key = package_name + "_" + package_version;
   PackageMap::iterator pi = package_map.find(key);
   if (pi != package_map.end()) {
-    P3DPackage *package = (*pi).second;
-    if (!package->get_failed()) {
-      return package;
+    // We've previously installed this package.
+    package = (*pi).second;
+
+    if (package->get_failed()) {
+      // If the package has previously failed, move it aside and try
+      // again (maybe it just failed because the user interrupted it).
+      nout << "Package " << key << " has previously failed; trying again.\n";
+      _failed_packages.push_back(package);
+      (*pi).second = NULL;
+      package = NULL;
     }
-
-    // If the package has previously failed, move it aside and try
-    // again (maybe it just failed because the user interrupted it).
-    nout << "Package " << key << " has previously failed; trying again.\n";
-    _failed_packages.push_back(package);
-    (*pi).second = NULL;
   }
 
-  P3DPackage *package = 
-    new P3DPackage(this, package_name, package_version, alt_host);
-  package_map[key] = package;
-
+  if (package == NULL) {
+    package = 
+      new P3DPackage(this, package_name, package_version, alt_host);
+    package_map[key] = package;
+  }
+    
   return package;
 }
 
@@ -694,3 +767,41 @@ copy_file(const string &from_filename, const string &to_filename) {
   unlink(temp_filename.c_str());
   return false;
 }
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::save_xml_file
+//       Access: Private, Static
+//  Description: Stores the XML document to the file named by
+//               to_filename, safely.
+////////////////////////////////////////////////////////////////////
+bool P3DHost::
+save_xml_file(TiXmlDocument *doc, const string &to_filename) {
+  // Save to a temporary file first, in case (a) we have different
+  // processes writing to the same file, and (b) to prevent partially
+  // overwriting the file should something go wrong.
+  ostringstream strm;
+  strm << to_filename << ".t";
+#ifdef _WIN32
+  strm << GetCurrentProcessId() << "_" << GetCurrentThreadId();
+#else
+  strm << getpid();
+#endif
+  string temp_filename = strm.str();
+
+  if (!doc->SaveFile(temp_filename.c_str())) {
+    unlink(temp_filename.c_str());
+    return false;
+  }
+
+  if (rename(temp_filename.c_str(), to_filename.c_str()) == 0) {
+    return true;
+  }
+
+  unlink(to_filename.c_str());
+  if (rename(temp_filename.c_str(), to_filename.c_str()) == 0) {
+    return true;
+  }
+
+  unlink(temp_filename.c_str());
+  return false;
+}

+ 4 - 1
direct/src/plugin/p3dHost.h

@@ -43,11 +43,12 @@ public:
   P3DHost *get_alt_host(const string &alt_host);
 
   inline bool has_contents_file() const;
+  bool has_current_contents_file(P3DInstanceManager *inst_mgr) const;
   inline int get_contents_seq() const;
   inline bool check_contents_hash(const string &pathname) const;
 
   bool read_contents_file();
-  bool read_contents_file(const string &contents_filename);
+  bool read_contents_file(const string &contents_filename, bool fresh_download);
   void read_xhost(TiXmlElement *xhost);
 
   P3DPackage *get_package(const string &package_name, 
@@ -72,6 +73,7 @@ private:
 
   static string standardize_filename(const string &filename);
   static bool copy_file(const string &from_filename, const string &to_filename);
+  static bool save_xml_file(TiXmlDocument *doc, const string &to_filename);
 
 private:
   string _host_dir;
@@ -80,6 +82,7 @@ private:
   string _download_url_prefix;
   string _descriptive_name;
   TiXmlElement *_xcontents;
+  time_t _contents_expiration;
   int _contents_seq;
   FileSpec _contents_spec;
 

+ 34 - 23
direct/src/plugin/p3dInstance.cxx

@@ -1141,6 +1141,11 @@ get_packages_info_ready() const {
 ////////////////////////////////////////////////////////////////////
 bool P3DInstance::
 get_packages_ready() const {
+  if (!_packages_specified) {
+    // We haven't even specified the full set of required packages yet.
+    return false;
+  }
+
   Packages::const_iterator pi;
   for (pi = _packages.begin(); pi != _packages.end(); ++pi) {
     if (!(*pi)->get_ready()) {
@@ -1384,6 +1389,7 @@ void P3DInstance::
 splash_button_clicked_main_thread() {
   if (is_failed()) {
     // Can't click the button after we've failed.
+    nout << "Ignoring click for failed instance\n";
     return;
   }
 
@@ -1391,6 +1397,8 @@ splash_button_clicked_main_thread() {
     auth_button_clicked();
   } else if (_session == NULL) {
     play_button_clicked();
+  } else {
+    nout << "Ignoring click for already-started instance\n";
   }
 }
 
@@ -2360,32 +2368,36 @@ handle_notify_request(const string &message) {
     // Once Python is up and running, we can get the actual main
     // object from the Python side, and merge it with our own.
 
-    // But only if this web page is allowed to call our scripting
-    // functions.
-    if (_matches_script_origin) {
-      TiXmlDocument *doc = new TiXmlDocument;
-      TiXmlElement *xcommand = new TiXmlElement("command");
-      xcommand->SetAttribute("cmd", "pyobj");
-      xcommand->SetAttribute("op", "get_panda_script_object");
-      doc->LinkEndChild(xcommand);
-      TiXmlDocument *response = _session->command_and_response(doc);
-      
-      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_p3dobj(xvalue);
-          }
+    TiXmlDocument *doc = new TiXmlDocument;
+    TiXmlElement *xcommand = new TiXmlElement("command");
+    xcommand->SetAttribute("cmd", "pyobj");
+    xcommand->SetAttribute("op", "get_panda_script_object");
+    doc->LinkEndChild(xcommand);
+    TiXmlDocument *response = _session->command_and_response(doc);
+
+    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_p3dobj(xvalue);
         }
-        delete response;
       }
-      
-      if (result != NULL) {
+      delete response;
+    }
+
+    if (result != NULL) {
+      if (_matches_script_origin) {
+        // We only actually merge the objects if this web page is
+        // allowed to call our scripting functions.
         _panda_script_object->set_pyobj(result);
-        P3D_OBJECT_DECREF(result);
+      } else {
+        // Otherwise, we just do a one-time application of the
+        // toplevel properties down to Python.
+        _panda_script_object->apply_properties(result);
       }
+      P3D_OBJECT_DECREF(result);
     }
 
     _panda_script_object->set_string_property("status", "starting");
@@ -3006,7 +3018,6 @@ mark_download_complete() {
 ////////////////////////////////////////////////////////////////////
 void P3DInstance::
 ready_to_start() {
-  nout << "_instance_started = " << _instance_started << "\n";
   if (_instance_started || is_failed()) {
     // Already started--or never mind.
     return;

+ 2 - 3
direct/src/plugin/p3dInstanceManager.cxx

@@ -261,12 +261,11 @@ initialize(int api_version, const string &contents_filename,
   create_runtime_environment();
   _is_initialized = true;
 
-  if (!_verify_contents &&
-      !host_url.empty() && !contents_filename.empty()) {
+  if (!host_url.empty() && !contents_filename.empty()) {
     // Attempt to pre-read the supplied contents.xml file, to avoid an
     // unnecessary download later.
     P3DHost *host = get_host(host_url);
-    if (!host->read_contents_file(contents_filename)) {
+    if (!host->read_contents_file(contents_filename, false)) {
       nout << "Couldn't read " << contents_filename << "\n";
     }
   }

+ 36 - 6
direct/src/plugin/p3dMainObject.cxx

@@ -289,12 +289,7 @@ set_pyobj(P3D_object *pyobj) {
 
       // Now that we have a pyobj, we have to transfer down all of the
       // properties we'd set locally.
-      Properties::const_iterator pi;
-      for (pi = _properties.begin(); pi != _properties.end(); ++pi) {
-        const string &property_name = (*pi).first;
-        P3D_object *value = (*pi).second;
-        P3D_OBJECT_SET_PROPERTY(_pyobj, property_name.c_str(), false, value);
-      }
+      apply_properties(_pyobj);
     }
   }
 }
@@ -310,6 +305,41 @@ get_pyobj() const {
   return _pyobj;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: P3DMainObject::apply_properties
+//       Access: Public
+//  Description: Applies the locally-set properties onto the indicated
+//               Python object, but does not store the object.  This
+//               is a one-time copy of the locally-set properties
+//               (like "coreapiHostUrl" and the like) onto the
+//               indicated Python object.
+////////////////////////////////////////////////////////////////////
+void P3DMainObject::
+apply_properties(P3D_object *pyobj) {
+  P3DPythonObject *p3dpyobj = NULL;
+  if (pyobj->_class == &P3DObject::_object_class) {
+    p3dpyobj = ((P3DObject *)pyobj)->as_python_object();
+  }
+
+  Properties::const_iterator pi;
+  for (pi = _properties.begin(); pi != _properties.end(); ++pi) {
+    const string &property_name = (*pi).first;
+    P3D_object *value = (*pi).second;
+    if (p3dpyobj != NULL && P3D_OBJECT_GET_TYPE(value) != P3D_OT_object) {
+      // If we know we have an actual P3DPythonObject (we really
+      // expect this), then we can call set_property_insecure()
+      // directly, because we want to allow setting the initial
+      // properties even if Javascript has no permissions to write
+      // into Python.  But we don't allow setting objects this way in
+      // any event.
+      p3dpyobj->set_property_insecure(property_name, false, value);
+    } else {
+      // Otherwise, we go through the generic interface.
+      P3D_OBJECT_SET_PROPERTY(pyobj, property_name.c_str(), false, value);
+    }
+  }
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DMainObject::set_instance
 //       Access: Public

+ 1 - 0
direct/src/plugin/p3dMainObject.h

@@ -65,6 +65,7 @@ public:
 
   void set_pyobj(P3D_object *pyobj);
   P3D_object *get_pyobj() const;
+  void apply_properties(P3D_object *pyobj);
 
   void set_instance(P3DInstance *inst);
 

+ 14 - 2
direct/src/plugin/p3dObject.cxx

@@ -352,7 +352,7 @@ fill_xml(TiXmlElement *xvalue, P3DSession *session) {
 
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DObject::get_object_array
-//       Access: Public
+//       Access: Public, Virtual
 //  Description: Returns a pointer to the array of objects represented
 //               by this object, if any, or NULL if the object does
 //               not represent an array of objects.  This may also
@@ -366,7 +366,7 @@ get_object_array() {
 
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DObject::get_object_array_size
-//       Access: Public
+//       Access: Public, Virtual
 //  Description: Returns the number of elements in the array returned
 //               by get_object_array(), or -1 if this object does not
 //               representan array of objects.
@@ -376,6 +376,18 @@ get_object_array_size() {
   return -1;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: P3DObject::as_python_object
+//       Access: Public, Virtual
+//  Description: Returns this object, downcast to a P3DPythonObject,
+//               if it is in fact an object of that type; or NULL if
+//               it is not.
+////////////////////////////////////////////////////////////////////
+P3DPythonObject *P3DObject::
+as_python_object() {
+  return NULL;
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DObject::get_bool_property
 //       Access: Public

+ 4 - 0
direct/src/plugin/p3dObject.h

@@ -17,6 +17,8 @@
 
 #include "p3d_plugin_common.h"
 
+class P3DPythonObject;
+
 ////////////////////////////////////////////////////////////////////
 //       Class : P3DObject
 // Description : The C++ implementation of P3D_value, corresponding
@@ -56,6 +58,8 @@ public:
   virtual P3D_object **get_object_array();
   virtual int get_object_array_size();
 
+  virtual P3DPythonObject *as_python_object();
+
   // Convenience functions.
   bool get_bool_property(const string &property);
   void set_bool_property(const string &property, bool value);

+ 28 - 9
direct/src/plugin/p3dPackage.cxx

@@ -181,6 +181,17 @@ void P3DPackage::
 add_instance(P3DInstance *inst) {
   _instances.push_back(inst);
 
+  P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
+  if (!_host->has_current_contents_file(inst_mgr)) {
+    // If the host needs to update its contents file, we're no longer
+    // sure that we're current.
+    _info_ready = false;
+    _ready = false;
+    _failed = false;
+    _allow_data_download = false;
+    nout << "No longer current: " << get_package_name() << "\n";
+  }
+  
   begin_info_download();
 }
 
@@ -381,13 +392,13 @@ begin_info_download() {
 void P3DPackage::
 download_contents_file() {
   P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
-  if (!_host->has_contents_file() && !inst_mgr->get_verify_contents()) {
-    // If we're allowed to read a contents file without checking the
-    // server first, try it now.
+  if (!_host->has_contents_file()) {
+    // First, read whatever contents file is already on disk.  Maybe
+    // it's current enough.
     _host->read_contents_file();
   }
 
-  if (_host->has_contents_file()) {
+  if (_host->has_current_contents_file(inst_mgr)) {
     // We've already got a contents.xml file; go straight to the
     // package desc file.
     host_got_contents_file();
@@ -414,14 +425,15 @@ download_contents_file() {
 ////////////////////////////////////////////////////////////////////
 void P3DPackage::
 contents_file_download_finished(bool success) {
-  if (!_host->has_contents_file()) {
-    if (!success || !_host->read_contents_file(_temp_contents_file->get_filename())) {
+  P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
+  if (!_host->has_current_contents_file(inst_mgr)) {
+    if (!success || !_host->read_contents_file(_temp_contents_file->get_filename(), true)) {
       nout << "Couldn't read " << *_temp_contents_file << "\n";
 
       // Maybe we can read an already-downloaded contents.xml file.
       string standard_filename = _host->get_host_dir() + "/contents.xml";
       if (_host->get_host_dir().empty() || 
-          !_host->read_contents_file(standard_filename)) {
+          !_host->read_contents_file(standard_filename, false)) {
         // Couldn't even read that.  Fail.
         report_done(false);
         delete _temp_contents_file;
@@ -503,7 +515,7 @@ contents_file_redownload_finished(bool success) {
     // from what we had before.
     if (!_host->check_contents_hash(_temp_contents_file->get_filename())) {
       // It changed!  Now see if we can read the new contents.
-      if (!_host->read_contents_file(_temp_contents_file->get_filename())) {
+      if (!_host->read_contents_file(_temp_contents_file->get_filename(), true)) {
         // Huh, appears to have changed to something bad.  Never mind.
         nout << "Couldn't read " << *_temp_contents_file << "\n";
 
@@ -569,7 +581,8 @@ host_got_contents_file() {
     // host.
     _alt_host.clear();
 
-    if (!_host->has_contents_file()) {
+    P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
+    if (!_host->has_current_contents_file(inst_mgr)) {
       // Now go back and get the contents.xml file for the new host.
       download_contents_file();
       return;
@@ -1434,6 +1447,12 @@ download_finished(bool success) {
     if (!_file_spec.full_verify(_package->_package_dir)) {
       nout << "After downloading " << get_url()
            << ", failed hash check\n";
+      nout << "expected: ";
+      _file_spec.output_hash(nout);
+      nout << "\n     got: ";
+      _file_spec.get_actual_file()->output_hash(nout);
+      nout << "\n";
+      
       success = false;
     }
   }

+ 40 - 2
direct/src/plugin/p3dPythonObject.cxx

@@ -159,6 +159,19 @@ set_property(const string &property, bool needs_response, P3D_object *value) {
     return false;
   }
 
+  return set_property_insecure(property, needs_response, value);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DPythonObject::set_property_insecure
+//       Access: Public
+//  Description: Works as set_property(), but does not check the
+//               matches_script_origin flag.  Intended to be called
+//               internally only, never to be called from Javascript.
+////////////////////////////////////////////////////////////////////
+bool P3DPythonObject::
+set_property_insecure(const string &property, bool needs_response, 
+                      P3D_object *value) {
   bool bresult = !needs_response;
 
   P3D_object *params[2];
@@ -168,12 +181,12 @@ set_property(const string &property, bool needs_response, P3D_object *value) {
 
   if (value == NULL) {
     // Delete an attribute.
-    result = call("__del_property__", needs_response, params, 1);
+    result = call_insecure("__del_property__", needs_response, params, 1);
 
   } else {
     // Set a new attribute.
     params[1] = value;
-    result = call("__set_property__", needs_response, params, 2);
+    result = call_insecure("__set_property__", needs_response, params, 2);
   }
 
   P3D_OBJECT_DECREF(params[0]);
@@ -244,6 +257,19 @@ call(const string &method_name, bool needs_response,
     return NULL;
   }
 
+  return call_insecure(method_name, needs_response, params, num_params);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DPythonObject::call_insecure
+//       Access: Public
+//  Description: Works as call(), but does not check the
+//               matches_script_origin flag.  Intended to be called
+//               internally only, never to be called from Javascript.
+////////////////////////////////////////////////////////////////////
+P3D_object *P3DPythonObject::
+call_insecure(const string &method_name, bool needs_response,
+              P3D_object *params[], int num_params) {
   TiXmlDocument *doc = new TiXmlDocument;
   TiXmlElement *xcommand = new TiXmlElement("command");
   xcommand->SetAttribute("cmd", "pyobj");
@@ -331,6 +357,18 @@ fill_xml(TiXmlElement *xvalue, P3DSession *session) {
   return false;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: P3DPythonObject::as_python_object
+//       Access: Public, Virtual
+//  Description: Returns this object, downcast to a P3DPythonObject,
+//               if it is in fact an object of that type; or NULL if
+//               it is not.
+////////////////////////////////////////////////////////////////////
+P3DPythonObject *P3DPythonObject::
+as_python_object() {
+  return this;
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DPythonObject::get_session
 //       Access: Public

+ 6 - 0
direct/src/plugin/p3dPythonObject.h

@@ -43,14 +43,20 @@ public:
 
   virtual P3D_object *get_property(const string &property);
   virtual bool set_property(const string &property, bool needs_response, P3D_object *value);
+  bool set_property_insecure(const string &property, bool needs_response,
+                             P3D_object *value);
 
   virtual bool has_method(const string &method_name);
   virtual P3D_object *call(const string &method_name, bool needs_response,
                            P3D_object *params[], int num_params);
+  P3D_object *call_insecure(const string &method_name, bool needs_response,
+                            P3D_object *params[], int num_params);
 
   virtual void output(ostream &out);
   virtual bool fill_xml(TiXmlElement *xvalue, P3DSession *session);
 
+  virtual P3DPythonObject *as_python_object();
+
   P3DSession *get_session();
   int get_object_id();
 

+ 6 - 0
direct/src/plugin/p3d_plugin.h

@@ -1096,6 +1096,12 @@ EXPCL_P3D_PLUGIN P3D_instance_handle_event_func P3D_instance_handle_event;
 
 #endif  /* P3D_FUNCTION_PROTOTYPES */
 
+// The default max_age, if none is specified in a particular
+// contents.xml, is 5 seconds.  This gives us enough time to start a
+// few packages downloading, without re-querying the host for a new
+// contents.xml at each operation.
+#define P3D_CONTENTS_DEFAULT_MAX_AGE 5
+
 #ifdef __cplusplus
 };  /* end of extern "C" */
 #endif

+ 125 - 48
direct/src/plugin_npapi/ppInstance.cxx

@@ -58,6 +58,7 @@ PPInstance(NPMIMEType pluginType, NPP instance, uint16_t mode,
   _window_handle_type = window_handle_type;
   _event_type = event_type;
   _script_object = NULL;
+  _contents_expiration = 0;
   _failed = false;
   _started = false;
 
@@ -171,24 +172,40 @@ begin() {
   }
 #endif  // __APPLE__
 
+  string url = PANDA_PACKAGE_HOST_URL;
+  if (!url.empty() && url[url.length() - 1] != '/') {
+    url += '/';
+  }
+  _download_url_prefix = url;
+
   if (!is_plugin_loaded() && !_failed) {
-    // Go download the contents file, so we can download the core DLL.
-    string url = PANDA_PACKAGE_HOST_URL;
-    if (!url.empty() && url[url.length() - 1] != '/') {
-      url += '/';
+    // We need to read the contents.xml file.  First, check to see if
+    // the version on disk is already current enough.
+    bool success = false;
+
+    string contents_filename = _root_dir + "/contents.xml";
+    if (read_contents_file(contents_filename, false)) {
+      if (time(NULL) < _contents_expiration) {
+        // Got the file, and it's good.
+        get_core_api();
+        success = true;
+      }
     }
-    _download_url_prefix = url;
-    ostringstream strm;
-    strm << url << "contents.xml";
-
-    // Append a uniquifying query string to the URL to force the
-    // download to go all the way through any caches.  We use the time
-    // in seconds; that's unique enough.
-    strm << "?" << time(NULL);
-    url = strm.str();
 
-    PPDownloadRequest *req = new PPDownloadRequest(PPDownloadRequest::RT_contents_file);
-    start_download(url, req);
+    if (!success) {
+      // Go download the latest contents.xml file.
+      ostringstream strm;
+      strm << _download_url_prefix << "contents.xml";
+      
+      // Append a uniquifying query string to the URL to force the
+      // download to go all the way through any caches.  We use the time
+      // in seconds; that's unique enough.
+      strm << "?" << time(NULL);
+      url = strm.str();
+      
+      PPDownloadRequest *req = new PPDownloadRequest(PPDownloadRequest::RT_contents_file);
+      start_download(url, req);
+    }
   }
 
   handle_request_loop();
@@ -562,7 +579,9 @@ url_notify(const char *url, NPReason reason, void *notifyData) {
         // there's an outstanding contents.xml file on disk, try to
         // load that one as a fallback.
         string contents_filename = _root_dir + "/contents.xml";
-        if (!read_contents_file(contents_filename)) {
+        if (read_contents_file(contents_filename, false)) {
+          get_core_api();
+        } else {
           nout << "Unable to read contents file " << contents_filename << "\n";
           set_failed();
         }
@@ -1134,18 +1153,58 @@ start_download(const string &url, PPDownloadRequest *req) {
 ////////////////////////////////////////////////////////////////////
 //     Function: PPInstance::read_contents_file
 //       Access: Private
-//  Description: Reads the contents.xml file and starts the core API
-//               DLL downloading, if necessary.
+//  Description: Attempts to open and read the contents.xml file on
+//               disk.  Copies the file to its standard location
+//               on success.  Returns true on success, false on
+//               failure.
 ////////////////////////////////////////////////////////////////////
 bool PPInstance::
-read_contents_file(const string &contents_filename) {
+read_contents_file(const string &contents_filename, bool fresh_download) {
   TiXmlDocument doc(contents_filename.c_str());
   if (!doc.LoadFile()) {
     return false;
   }
 
+  bool found_core_package = false;
+
   TiXmlElement *xcontents = doc.FirstChildElement("contents");
   if (xcontents != NULL) {
+    int max_age = P3D_CONTENTS_DEFAULT_MAX_AGE;
+    xcontents->Attribute("max_age", &max_age);
+
+    // Get the latest possible expiration time, based on the max_age
+    // indication.  Any expiration time later than this is in error.
+    time_t now = time(NULL);
+    _contents_expiration = now + (time_t)max_age;
+
+    if (fresh_download) {
+      // Update the XML with the new download information.
+      TiXmlElement *xorig = xcontents->FirstChildElement("orig");
+      while (xorig != NULL) {
+        xcontents->RemoveChild(xorig);
+        xorig = xcontents->FirstChildElement("orig");
+      }
+
+      xorig = new TiXmlElement("orig");
+      xcontents->LinkEndChild(xorig);
+      
+      xorig->SetAttribute("expiration", (int)_contents_expiration);
+
+    } else {
+      // Read the expiration time from the XML.
+      int expiration = 0;
+      TiXmlElement *xorig = xcontents->FirstChildElement("orig");
+      if (xorig != NULL) {
+        xorig->Attribute("expiration", &expiration);
+      }
+      
+      _contents_expiration = min(_contents_expiration, (time_t)expiration);
+    }
+
+    nout << "read contents.xml, max_age = " << max_age
+         << ", expires in " << max(_contents_expiration, now) - now
+         << " s\n";
+
     // Look for the <host> entry; it might point us at a different
     // download URL, and it might mention some mirrors.
     find_host(xcontents);
@@ -1157,19 +1216,33 @@ read_contents_file(const string &contents_filename) {
       if (name != NULL && strcmp(name, "coreapi") == 0) {
         const char *platform = xpackage->Attribute("platform");
         if (platform != NULL && strcmp(platform, DTOOL_PLATFORM) == 0) {
-          get_core_api(xpackage);
-          return true;
+          _core_api_dll.load_xml(xpackage);
+          found_core_package = true;
+          break;
         }
       }
-    
+        
       xpackage = xpackage->NextSiblingElement("package");
     }
   }
 
-  // Couldn't find the coreapi package description.
-  nout << "No coreapi package defined in contents file for "
-       << DTOOL_PLATFORM << "\n";
-  return false;
+  if (!found_core_package) {
+    // Couldn't find the coreapi package description.
+    nout << "No coreapi package defined in contents file for "
+         << DTOOL_PLATFORM << "\n";
+    return false;
+  }
+
+  // Success.  Now save the file in its proper place.
+  string standard_filename = _root_dir + "/contents.xml";
+
+  mkfile_complete(standard_filename, nout);
+  if (!doc.SaveFile(standard_filename.c_str())) {
+    nout << "Couldn't rewrite " << standard_filename << "\n";
+    return false;
+  }
+  
+  return true;
 }
 
 ////////////////////////////////////////////////////////////////////
@@ -1212,24 +1285,28 @@ void PPInstance::
 downloaded_file(PPDownloadRequest *req, const string &filename) {
   switch (req->_rtype) {
   case PPDownloadRequest::RT_contents_file:
-    // Now we have the contents.xml file.  Read this to get the
-    // filename and md5 hash of our core API DLL.
-    if (read_contents_file(filename)) {
-      // Successfully read.  Copy it into its normal place.
-      string contents_filename = _root_dir + "/contents.xml";
-      copy_file(filename, contents_filename);
-      
-    } else {
-      // Error reading the contents.xml file, or in loading the core
-      // API that it references.
-      nout << "Unable to read contents file " << filename << "\n";
-
-      // If there's an outstanding contents.xml file on disk, try to
-      // load that one as a fallback.
-      string contents_filename = _root_dir + "/contents.xml";
-      if (!read_contents_file(contents_filename)) {
-        nout << "Unable to read contents file " << contents_filename << "\n";
-        set_failed();
+    {
+      // Now we have the contents.xml file.  Read this to get the
+      // filename and md5 hash of our core API DLL.
+      if (read_contents_file(filename, true)) {
+        // Successfully downloaded and read, and it has been written
+        // into its normal place.
+        get_core_api();
+        
+      } else {
+        // Error reading the contents.xml file, or in loading the core
+        // API that it references.
+        nout << "Unable to read contents file " << filename << "\n";
+        
+        // If there's an outstanding contents.xml file on disk, try to
+        // load that one as a fallback.
+        string contents_filename = _root_dir + "/contents.xml";
+        if (read_contents_file(contents_filename, false)) {
+          get_core_api();
+        } else {
+          nout << "Unable to read contents file " << contents_filename << "\n";
+          set_failed();
+        }
       }
     }
     break;
@@ -1350,9 +1427,7 @@ send_p3d_temp_file_data() {
 //               if necessary.
 ////////////////////////////////////////////////////////////////////
 void PPInstance::
-get_core_api(TiXmlElement *xpackage) {
-  _core_api_dll.load_xml(xpackage);
-
+get_core_api() {
   if (_core_api_dll.quick_verify(_root_dir)) {
     // The DLL file is good.  Just load it.
     do_load_plugin();
@@ -1477,7 +1552,9 @@ do_load_plugin() {
 #endif  // P3D_PLUGIN_P3D_PLUGIN
 
   nout << "Attempting to load core API from " << pathname << "\n";
-  if (!load_plugin(pathname, "", "", true, "", "", "", false, false, 
+  string contents_filename = _root_dir + "/contents.xml";
+  if (!load_plugin(pathname, contents_filename, PANDA_PACKAGE_HOST_URL,
+                   true, "", "", "", false, false, 
                    _root_dir, nout)) {
     nout << "Unable to launch core API in " << pathname << "\n";
     set_failed();

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

@@ -84,8 +84,8 @@ private:
   void open_p3d_temp_file();
   void send_p3d_temp_file_data();
 
-  bool read_contents_file(const string &contents_filename);
-  void get_core_api(TiXmlElement *xpackage);
+  bool read_contents_file(const string &contents_filename, bool fresh_download);
+  void get_core_api();
   void downloaded_plugin(const string &filename);
   void do_load_plugin();
 
@@ -140,6 +140,7 @@ private:
   CoreUrls _core_urls;
 
   FileSpec _core_api_dll;
+  time_t _contents_expiration;
   bool _failed;
   bool _started;
 

+ 147 - 74
direct/src/plugin_standalone/panda3d.cxx

@@ -353,99 +353,143 @@ post_arg_processing() {
 bool Panda3D::
 get_plugin() {
   // First, look for the existing contents.xml file.
+  bool success = false;
+
   Filename contents_filename = Filename(Filename::from_os_specific(_root_dir), "contents.xml");
-  if (!_verify_contents && read_contents_file(contents_filename)) {
-    // Got the file, and it's good.
-    return true;
+  if (read_contents_file(contents_filename, false)) {
+    if (!_verify_contents || time(NULL) < _contents_expiration) {
+      // Got the file, and it's good.
+      success = true;
+    }
   }
 
-  // Couldn't read it, so go get it.
-  HTTPClient *http = HTTPClient::get_global_ptr();
+  if (!success) {
+    // Couldn't read it (or it wasn't current enough), so go get a new
+    // one.
+    HTTPClient *http = HTTPClient::get_global_ptr();
+    
+    // Try the super_mirror first.
+    if (!_super_mirror_url_prefix.empty()) {
+      // We don't bother putting a uniquifying query string when we're
+      // downloading this file from the super_mirror.  The super_mirror
+      // is by definition a cache, so it doesn't make sense to bust
+      // caches here.
+      string url = _super_mirror_url_prefix + "contents.xml";
+      PT(HTTPChannel) channel = http->make_channel(false);
+      channel->get_document(url);
+      
+      Filename tempfile = Filename::temporary("", "p3d_");
+      if (!channel->download_to_file(tempfile)) {
+        cerr << "Unable to download " << url << "\n";
+        tempfile.unlink();
+      } else {
+        // Successfully downloaded from the super_mirror; try to read it.
+        success = read_contents_file(tempfile, true);
+        tempfile.unlink();
+      }
+    }
 
-  // Try the super_mirror first.
-  if (!_super_mirror_url_prefix.empty()) {
-    // We don't bother putting a uniquifying query string when we're
-    // downloading this file from the super_mirror.  The super_mirror
-    // is by definition a cache, so it doesn't make sense to bust
-    // caches here.
-    string url = _super_mirror_url_prefix + "contents.xml";
-    PT(HTTPChannel) channel = http->make_channel(false);
-    channel->get_document(url);
+    if (!success) {
+      // Go download contents.xml from the actual host.
+      ostringstream strm;
+      strm << _host_url_prefix << "contents.xml";
+      // Append a uniquifying query string to the URL to force the
+      // download to go all the way through any caches.  We use the time
+      // in seconds; that's unique enough.
+      strm << "?" << time(NULL);
+      string url = strm.str();
+      
+      // We might as well explicitly request the cache to be disabled too,
+      // since we have an interface for that via HTTPChannel.
+      DocumentSpec request(url);
+      request.set_cache_control(DocumentSpec::CC_no_cache);
+      
+      PT(HTTPChannel) channel = http->make_channel(false);
+      channel->get_document(request);
+      
+      // Since we have to download some of it, might as well ask the core
+      // API to check all of it.
+      _verify_contents = true;
+      
+      // First, download it to a temporary file.
+      Filename tempfile = Filename::temporary("", "p3d_");
+      if (!channel->download_to_file(tempfile)) {
+        cerr << "Unable to download " << url << "\n";
+        
+        // Couldn't download, but try to read the existing contents.xml
+        // file anyway.  Maybe it's good enough.
+        success = read_contents_file(contents_filename, false);
+        
+      } else {
+        // Successfully downloaded; read it and move it into place.
+        success = read_contents_file(tempfile, true);
+      }
 
-    Filename tempfile = Filename::temporary("", "p3d_");
-    if (!channel->download_to_file(tempfile)) {
-      cerr << "Unable to download " << url << "\n";
       tempfile.unlink();
-    } else {
-      // Successfully downloaded from the super_mirror; move it into
-      // place and try to read it.
-      contents_filename.make_dir();
-      contents_filename.unlink();
-      tempfile.rename_to(contents_filename);
-      if (read_contents_file(contents_filename)) {
-        return true;
-      }
     }
-
-    // Failed to download from the super_mirror.
   }
 
-  // Go download contents.xml from the actual host.
-  ostringstream strm;
-  strm << _host_url_prefix << "contents.xml";
-  // Append a uniquifying query string to the URL to force the
-  // download to go all the way through any caches.  We use the time
-  // in seconds; that's unique enough.
-  strm << "?" << time(NULL);
-  string url = strm.str();
-
-  // We might as well explicitly request the cache to be disabled too,
-  // since we have an interface for that via HTTPChannel.
-  DocumentSpec request(url);
-  request.set_cache_control(DocumentSpec::CC_no_cache);
-
-  PT(HTTPChannel) channel = http->make_channel(false);
-  channel->get_document(request);
-
-  // First, download it to a temporary file.
-  Filename tempfile = Filename::temporary("", "p3d_");
-  if (!channel->download_to_file(tempfile)) {
-    cerr << "Unable to download " << url << "\n";
-    tempfile.unlink();
-
-    // Couldn't download, but fall through and try to read the
-    // contents.xml file anyway.  Maybe it's good enough.
-  } else {
-    // Successfully downloaded; move the temporary file into place.
-    contents_filename.make_dir();
-    contents_filename.unlink();
-    tempfile.rename_to(contents_filename);
+  if (success) {
+    // Now that we've downloaded the contents file successfully, start
+    // the Core API.
+    success = get_core_api();
   }
 
-  // Since we had to download some of it, might as well ask the core
-  // API to check all of it.
-  _verify_contents = true;
-
-  return read_contents_file(contents_filename);
+  return success;
 }
 
 ////////////////////////////////////////////////////////////////////
 //     Function: Panda3D::read_contents_file
 //       Access: Protected
 //  Description: Attempts to open and read the contents.xml file on
-//               disk, and uses that data to load the plugin, if
-//               possible.  Returns true on success, false on failure.
+//               disk.  Copies the file to its standard location
+//               on success.  Returns true on success, false on
+//               failure.
 ////////////////////////////////////////////////////////////////////
 bool Panda3D::
-read_contents_file(const Filename &contents_filename) {
+read_contents_file(const Filename &contents_filename, bool fresh_download) {
   string os_contents_filename = contents_filename.to_os_specific();
   TiXmlDocument doc(os_contents_filename.c_str());
   if (!doc.LoadFile()) {
     return false;
   }
 
+  bool found_core_package = false;
+
   TiXmlElement *xcontents = doc.FirstChildElement("contents");
   if (xcontents != NULL) {
+    int max_age = P3D_CONTENTS_DEFAULT_MAX_AGE;
+    xcontents->Attribute("max_age", &max_age);
+
+    // Get the latest possible expiration time, based on the max_age
+    // indication.  Any expiration time later than this is in error.
+    time_t now = time(NULL);
+    _contents_expiration = now + (time_t)max_age;
+
+    if (fresh_download) {
+      // Update the XML with the new download information.
+      TiXmlElement *xorig = xcontents->FirstChildElement("orig");
+      while (xorig != NULL) {
+        xcontents->RemoveChild(xorig);
+        xorig = xcontents->FirstChildElement("orig");
+      }
+
+      xorig = new TiXmlElement("orig");
+      xcontents->LinkEndChild(xorig);
+      
+      xorig->SetAttribute("expiration", (int)_contents_expiration);
+
+    } else {
+      // Read the expiration time from the XML.
+      int expiration = 0;
+      TiXmlElement *xorig = xcontents->FirstChildElement("orig");
+      if (xorig != NULL) {
+        xorig->Attribute("expiration", &expiration);
+      }
+      
+      _contents_expiration = min(_contents_expiration, (time_t)expiration);
+    }
+
     // Look for the <host> entry; it might point us at a different
     // download URL, and it might mention some mirrors.
     find_host(xcontents);
@@ -457,7 +501,9 @@ read_contents_file(const Filename &contents_filename) {
       if (name != NULL && strcmp(name, "coreapi") == 0) {
         const char *platform = xpackage->Attribute("platform");
         if (platform != NULL && _this_platform == string(platform)) {
-          return get_core_api(contents_filename, xpackage);
+          _core_api_dll.load_xml(xpackage);
+          found_core_package = true;
+          break;
         }
       }
     
@@ -465,10 +511,38 @@ read_contents_file(const Filename &contents_filename) {
     }
   }
 
-  // Couldn't find the coreapi package description.
-  nout << "No coreapi package defined in contents file for "
-       << _this_platform << "\n";
-  return false;
+  if (!found_core_package) {
+    // Couldn't find the coreapi package description.
+    nout << "No coreapi package defined in contents file for "
+         << _this_platform << "\n";
+    return false;
+  }
+
+  // Success.  Now copy the file into place.
+  Filename standard_filename = Filename(Filename::from_os_specific(_root_dir), "contents.xml");
+  if (fresh_download) {
+    Filename tempfile = Filename::temporary("", "p3d_");
+    string os_specific = tempfile.to_os_specific();
+    if (!doc.SaveFile(os_specific.c_str())) {
+      nout << "Couldn't write to " << tempfile << "\n";
+      tempfile.unlink();
+      return false;
+    }
+    tempfile.rename_to(standard_filename);
+    nout << "rewrote " << standard_filename << "\n";
+
+  } else {
+    if (contents_filename != standard_filename) {
+      if (!contents_filename.rename_to(standard_filename)) {
+        nout << "Couldn't move contents.xml to " << standard_filename << "\n";
+        contents_filename.unlink();
+        return false;
+      }
+      nout << "moved to " << standard_filename << "\n";
+    }
+  }
+
+  return true;
 }
 
 ////////////////////////////////////////////////////////////////////
@@ -598,9 +672,7 @@ choose_random_mirrors(vector_string &result, int num_mirrors) {
 //               if necessary.
 ////////////////////////////////////////////////////////////////////
 bool Panda3D::
-get_core_api(const Filename &contents_filename, TiXmlElement *xpackage) {
-  _core_api_dll.load_xml(xpackage);
-
+get_core_api() {
   if (!_core_api_dll.quick_verify(_root_dir)) {
     // The DLL file needs to be downloaded.  Build up our list of
     // URL's to attempt to download it from, in reverse order.
@@ -690,6 +762,7 @@ get_core_api(const Filename &contents_filename, TiXmlElement *xpackage) {
 
   bool trusted_environment = !_enable_security;
 
+  Filename contents_filename = Filename(Filename::from_os_specific(_root_dir), "contents.xml");
   if (!load_plugin(pathname, contents_filename.to_os_specific(),
                    _host_url, _verify_contents, _this_platform, _log_dirname,
                    _log_basename, trusted_environment, _console_environment,

+ 2 - 2
direct/src/plugin_standalone/panda3d.h

@@ -37,12 +37,12 @@ public:
 protected:
   bool post_arg_processing();
   bool get_plugin();
-  bool read_contents_file(const Filename &contents_filename);
+  bool read_contents_file(const Filename &contents_filename, bool fresh_download);
   void find_host(TiXmlElement *xcontents);
   void read_xhost(TiXmlElement *xhost);
   void add_mirror(string mirror_url);
   void choose_random_mirrors(vector_string &result, int num_mirrors);
-  bool get_core_api(const Filename &contents_filename, TiXmlElement *xplugin);
+  bool get_core_api();
 
   void usage();
 

+ 1 - 0
direct/src/plugin_standalone/panda3dBase.cxx

@@ -65,6 +65,7 @@ Panda3DBase(bool console_environment) {
   _host_url = PANDA_PACKAGE_HOST_URL;
   _this_platform = DTOOL_PLATFORM;
   _verify_contents = false;
+  _contents_expiration = 0;
 
   // Seed the lame random number generator in rand(); we use it to
   // select a mirror for downloading.

+ 2 - 0
direct/src/plugin_standalone/panda3dBase.h

@@ -74,6 +74,8 @@ protected:
   string _log_basename;
   string _this_platform;
   bool _verify_contents;
+  time_t _contents_expiration;
+
   P3D_window_type _window_type;
   P3D_window_handle _parent_window;
   int _win_x, _win_y;