586 lines
20 KiB
C++
586 lines
20 KiB
C++
// Filename: p3dHost.cxx
|
|
// Created by: drose (21Aug09)
|
|
//
|
|
////////////////////////////////////////////////////////////////////
|
|
//
|
|
// PANDA 3D SOFTWARE
|
|
// Copyright (c) Carnegie Mellon University. All rights reserved.
|
|
//
|
|
// All use of this software is subject to the terms of the revised BSD
|
|
// license. You should have received a copy of this license along
|
|
// with this source code in a file named "LICENSE."
|
|
//
|
|
////////////////////////////////////////////////////////////////////
|
|
|
|
#include "p3dHost.h"
|
|
#include "p3dInstanceManager.h"
|
|
#include "p3dPackage.h"
|
|
#include "openssl/md5.h"
|
|
|
|
#include <algorithm>
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::Constructor
|
|
// Access: Private
|
|
// Description: Use P3DInstanceManager::get_host() to construct a new
|
|
// P3DHost.
|
|
////////////////////////////////////////////////////////////////////
|
|
P3DHost::
|
|
P3DHost(const string &host_url) :
|
|
_host_url(host_url)
|
|
{
|
|
// Ensure that the download URL ends with a slash.
|
|
_host_url_prefix = _host_url;
|
|
if (!_host_url_prefix.empty() && _host_url_prefix[_host_url_prefix.size() - 1] != '/') {
|
|
_host_url_prefix += "/";
|
|
}
|
|
_download_url_prefix = _host_url_prefix;
|
|
|
|
_xcontents = NULL;
|
|
_contents_seq = 0;
|
|
|
|
determine_host_dir();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::Destructor
|
|
// Access: Private
|
|
// Description:
|
|
////////////////////////////////////////////////////////////////////
|
|
P3DHost::
|
|
~P3DHost() {
|
|
if (_xcontents != NULL) {
|
|
delete _xcontents;
|
|
}
|
|
|
|
Packages::iterator mi;
|
|
for (mi = _packages.begin(); mi != _packages.end(); ++mi) {
|
|
PackageMap &package_map = (*mi).second;
|
|
PackageMap::iterator pi;
|
|
for (pi = package_map.begin(); pi != package_map.end(); ++pi) {
|
|
delete (*pi).second;
|
|
}
|
|
}
|
|
_packages.clear();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::get_alt_host
|
|
// Access: Public
|
|
// Description: Returns the pre-defined alternate host with the
|
|
// indicated token, if one is defined for this token, or
|
|
// the original host if there is no alternate host
|
|
// defined for this token.
|
|
//
|
|
// This is intended to implement test versions and the
|
|
// like, for instance in which a particular p3d file may
|
|
// reference a package on one particular host, but there
|
|
// is an alternate version to be tested on a different
|
|
// host. The HTML code that embeds the p3d file can
|
|
// choose to set the alt_host token to redirect the p3d
|
|
// file to the alternate host.
|
|
//
|
|
// The actual URL for the alternate host is embedded
|
|
// within the host's contents.xml as a security measure,
|
|
// to prevent people from tricking a p3d file into
|
|
// running untrusted code by redirecting it to an
|
|
// arbitrary URL.
|
|
////////////////////////////////////////////////////////////////////
|
|
P3DHost *P3DHost::
|
|
get_alt_host(const string &alt_host) {
|
|
assert(_xcontents != NULL);
|
|
|
|
AltHosts::iterator hi;
|
|
hi = _alt_hosts.find(alt_host);
|
|
if (hi != _alt_hosts.end() && (*hi).second != _host_url) {
|
|
P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
|
|
return inst_mgr->get_host((*hi).second);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::read_contents_file
|
|
// Access: Public
|
|
// Description: Reads the contents.xml file in the standard
|
|
// filename, if possible.
|
|
//
|
|
// Returns true on success, false on failure.
|
|
////////////////////////////////////////////////////////////////////
|
|
bool P3DHost::
|
|
read_contents_file() {
|
|
string standard_filename = _host_dir + "/contents.xml";
|
|
return read_contents_file(standard_filename);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::read_contents_file
|
|
// Access: Public
|
|
// Description: Reads the contents.xml file in the indicated
|
|
// filename. On success, copies 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) {
|
|
TiXmlDocument doc(contents_filename.c_str());
|
|
if (!doc.LoadFile()) {
|
|
return false;
|
|
}
|
|
|
|
TiXmlElement *xcontents = doc.FirstChildElement("contents");
|
|
if (xcontents == NULL) {
|
|
return false;
|
|
}
|
|
|
|
if (_xcontents != NULL) {
|
|
delete _xcontents;
|
|
}
|
|
_xcontents = (TiXmlElement *)xcontents->Clone();
|
|
++_contents_seq;
|
|
_contents_spec.read_hash(contents_filename);
|
|
|
|
TiXmlElement *xhost = _xcontents->FirstChildElement("host");
|
|
if (xhost != NULL) {
|
|
const char *url = xhost->Attribute("url");
|
|
if (url != NULL && _host_url == string(url)) {
|
|
// We're the primary host. This is the normal case.
|
|
read_xhost(xhost);
|
|
|
|
// Build up the list of alternate hosts.
|
|
TiXmlElement *xalthost = xhost->FirstChildElement("alt_host");
|
|
while (xalthost != NULL) {
|
|
const char *keyword = xalthost->Attribute("keyword");
|
|
const char *url = xalthost->Attribute("url");
|
|
if (keyword != NULL && url != NULL) {
|
|
_alt_hosts[keyword] = url;
|
|
}
|
|
xalthost = xalthost->NextSiblingElement("alt_host");
|
|
}
|
|
|
|
} else {
|
|
// We're not the primary host; perhaps we're an alternate host.
|
|
TiXmlElement *xalthost = xhost->FirstChildElement("alt_host");
|
|
while (xalthost != NULL) {
|
|
const char *url = xalthost->Attribute("url");
|
|
if (url != NULL && _host_url == string(url)) {
|
|
// Yep, we're this alternate host.
|
|
read_xhost(xhost);
|
|
break;
|
|
}
|
|
xalthost = xalthost->NextSiblingElement("alt_host");
|
|
}
|
|
}
|
|
}
|
|
|
|
string standard_filename = _host_dir + "/contents.xml";
|
|
if (standardize_filename(standard_filename) !=
|
|
standardize_filename(contents_filename)) {
|
|
copy_file(contents_filename, standard_filename);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::get_package
|
|
// Access: Public
|
|
// Description: Returns a (possibly shared) pointer to the indicated
|
|
// package.
|
|
////////////////////////////////////////////////////////////////////
|
|
P3DPackage *P3DHost::
|
|
get_package(const string &package_name, const string &package_version,
|
|
const string &alt_host) {
|
|
if (!alt_host.empty()) {
|
|
if (_xcontents != NULL) {
|
|
// If we're asking for an alt host and we've already read our
|
|
// contents.xml file, then we already know all of our hosts, and
|
|
// we can start the package off with the correct host immediately.
|
|
P3DHost *new_host = get_alt_host(alt_host);
|
|
return new_host->get_package(package_name, package_version);
|
|
}
|
|
|
|
// If we haven't read contents.xml yet, we need to create the
|
|
// package first, then let it be responsible for downloading our
|
|
// contents.xml, and it can migrate to its alt_host after that.
|
|
}
|
|
|
|
PackageMap &package_map = _packages[alt_host];
|
|
|
|
string key = package_name + "_" + package_version;
|
|
PackageMap::iterator pi = package_map.find(key);
|
|
if (pi != package_map.end()) {
|
|
return (*pi).second;
|
|
}
|
|
|
|
P3DPackage *package =
|
|
new P3DPackage(this, package_name, package_version, alt_host);
|
|
bool inserted = package_map.insert(PackageMap::value_type(key, package)).second;
|
|
assert(inserted);
|
|
|
|
return package;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::get_package_desc_file
|
|
// Access: Public
|
|
// Description: Fills the indicated FileSpec with the hash
|
|
// information for the package's desc file, and also
|
|
// determines the package's platform. Returns true if
|
|
// successful, false if the package is unknown. This
|
|
// requires has_contents_file() to return true in order
|
|
// to be successful.
|
|
////////////////////////////////////////////////////////////////////
|
|
bool P3DHost::
|
|
get_package_desc_file(FileSpec &desc_file, // out
|
|
string &package_platform, // out
|
|
bool &package_solo, // out
|
|
const string &package_name, // in
|
|
const string &package_version) { // in
|
|
if (_xcontents == NULL) {
|
|
return false;
|
|
}
|
|
|
|
P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
|
|
|
|
// Scan the contents data for the indicated package. First, we look
|
|
// for a platform-specific version.
|
|
TiXmlElement *xpackage = _xcontents->FirstChildElement("package");
|
|
while (xpackage != NULL) {
|
|
const char *name = xpackage->Attribute("name");
|
|
const char *platform = xpackage->Attribute("platform");
|
|
const char *version = xpackage->Attribute("version");
|
|
const char *solo = xpackage->Attribute("solo");
|
|
if (version == NULL) {
|
|
version = "";
|
|
}
|
|
if (name != NULL && platform != NULL &&
|
|
package_name == name &&
|
|
inst_mgr->get_platform() == platform &&
|
|
package_version == version) {
|
|
// Here's the matching package definition.
|
|
desc_file.load_xml(xpackage);
|
|
package_platform = platform;
|
|
package_solo = false;
|
|
if (solo != NULL) {
|
|
package_solo = (atoi(solo) != 0);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
xpackage = xpackage->NextSiblingElement("package");
|
|
}
|
|
|
|
// Look again, this time looking for a non-platform-specific version.
|
|
xpackage = _xcontents->FirstChildElement("package");
|
|
while (xpackage != NULL) {
|
|
const char *name = xpackage->Attribute("name");
|
|
const char *platform = xpackage->Attribute("platform");
|
|
const char *version = xpackage->Attribute("version");
|
|
const char *solo = xpackage->Attribute("solo");
|
|
if (platform == NULL) {
|
|
platform = "";
|
|
}
|
|
if (version == NULL) {
|
|
version = "";
|
|
}
|
|
if (name != NULL &&
|
|
package_name == name &&
|
|
*platform == '\0' &&
|
|
package_version == version) {
|
|
// Here's the matching package definition.
|
|
desc_file.load_xml(xpackage);
|
|
package_platform = platform;
|
|
package_solo = false;
|
|
if (solo != NULL) {
|
|
package_solo = (atoi(solo) != 0);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
xpackage = xpackage->NextSiblingElement("package");
|
|
}
|
|
|
|
// Couldn't find the named package.
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::migrate_package
|
|
// Access: Public
|
|
// Description: This is called by P3DPackage when it migrates from
|
|
// this host to its final alt_host, after downloading
|
|
// the contents.xml file for this file and learning the
|
|
// true URL for its target alt_host.
|
|
////////////////////////////////////////////////////////////////////
|
|
void P3DHost::
|
|
migrate_package(P3DPackage *package, const string &alt_host, P3DHost *new_host) {
|
|
assert(new_host != this);
|
|
assert(new_host == get_alt_host(alt_host));
|
|
|
|
string key = package->get_package_name() + "_" + package->get_package_version();
|
|
|
|
PackageMap &package_map = _packages[alt_host];
|
|
PackageMap::iterator pi = package_map.find(key);
|
|
assert(pi != package_map.end());
|
|
package_map.erase(pi);
|
|
|
|
PackageMap &new_package_map = new_host->_packages[""];
|
|
bool inserted = new_package_map.insert(PackageMap::value_type(key, package)).second;
|
|
assert(inserted);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::choose_random_mirrors
|
|
// Access: Public
|
|
// Description: Selects num_mirrors elements, chosen at random, from
|
|
// the _mirrors list. Adds the selected mirrors to
|
|
// result. If there are fewer than num_mirrors elements
|
|
// in the list, adds only as many mirrors as we can get.
|
|
////////////////////////////////////////////////////////////////////
|
|
void P3DHost::
|
|
choose_random_mirrors(vector<string> &result, int num_mirrors) {
|
|
vector<size_t> selected;
|
|
|
|
size_t num_to_select = min(_mirrors.size(), (size_t)num_mirrors);
|
|
while (num_to_select > 0) {
|
|
size_t i = (size_t)(((double)rand() / (double)RAND_MAX) * _mirrors.size());
|
|
while (find(selected.begin(), selected.end(), i) != selected.end()) {
|
|
// Already found this i, find a new one.
|
|
i = (size_t)(((double)rand() / (double)RAND_MAX) * _mirrors.size());
|
|
}
|
|
selected.push_back(i);
|
|
result.push_back(_mirrors[i]);
|
|
--num_to_select;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::add_mirror
|
|
// Access: Public
|
|
// Description: Adds a new URL to serve as a mirror for this host.
|
|
// The mirrors will be consulted first, before
|
|
// consulting the host directly.
|
|
////////////////////////////////////////////////////////////////////
|
|
void P3DHost::
|
|
add_mirror(string mirror_url) {
|
|
// Ensure the URL ends in a slash.
|
|
if (!mirror_url.empty() && mirror_url[mirror_url.size() - 1] != '/') {
|
|
mirror_url += '/';
|
|
}
|
|
|
|
// Add it to the _mirrors list, but only if it's not already
|
|
// there.
|
|
if (find(_mirrors.begin(), _mirrors.end(), mirror_url) == _mirrors.end()) {
|
|
_mirrors.push_back(mirror_url);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::determine_host_dir
|
|
// Access: Private
|
|
// Description: Hashes the host_url into a (mostly) unique directory
|
|
// string, which will be the root of the host's install
|
|
// tree. Stores the result in _host_dir.
|
|
//
|
|
// This code is duplicated in Python, in
|
|
// HostInfo.determineHostDir().
|
|
////////////////////////////////////////////////////////////////////
|
|
void P3DHost::
|
|
determine_host_dir() {
|
|
P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
|
|
_host_dir = inst_mgr->get_root_dir();
|
|
_host_dir += "/";
|
|
|
|
string hostname;
|
|
|
|
// Look for a server name in the URL. Including this string in the
|
|
// directory name makes it friendlier for people browsing the
|
|
// directory.
|
|
|
|
// We can't use URLSpec here, because we don't link with Panda3D.
|
|
// We have to do it by hand.
|
|
size_t p = _host_url.find("://");
|
|
if (p != string::npos) {
|
|
size_t start = p + 3;
|
|
size_t end = _host_url.find("/", start);
|
|
// Now start .. end is something like "username@host:port".
|
|
|
|
size_t at = _host_url.find("@", start);
|
|
if (at < end) {
|
|
start = at + 1;
|
|
}
|
|
|
|
size_t colon = _host_url.find(":", start);
|
|
if (colon < end) {
|
|
end = colon;
|
|
}
|
|
|
|
// Now start .. end is just the hostname.
|
|
hostname = _host_url.substr(start, end - start);
|
|
}
|
|
|
|
// Now build a hash string of the whole URL. We'll use MD5 to get a
|
|
// pretty good hash, with a minimum chance of collision. Even if
|
|
// there is a hash collision, though, it's not the end of the world;
|
|
// it just means that both hosts will dump their packages into the
|
|
// same directory, and they'll fight over the toplevel contents.xml
|
|
// file. Assuming they use different version numbers (which should
|
|
// be safe since they have the same hostname), there will be minimal
|
|
// redownloading.
|
|
|
|
static const size_t hash_size = 16;
|
|
unsigned char md[hash_size];
|
|
|
|
size_t keep_hash = hash_size;
|
|
|
|
if (!hostname.empty()) {
|
|
_host_dir += hostname;
|
|
_host_dir += "_";
|
|
|
|
// If we successfully got a hostname, we don't really need the
|
|
// full hash. We'll keep half of it.
|
|
keep_hash = keep_hash / 2;
|
|
}
|
|
|
|
MD5_CTX ctx;
|
|
MD5_Init(&ctx);
|
|
MD5_Update(&ctx, _host_url.data(), _host_url.size());
|
|
MD5_Final(md, &ctx);
|
|
|
|
for (size_t i = 0; i < keep_hash; ++i) {
|
|
int high = (md[i] >> 4) & 0xf;
|
|
int low = md[i] & 0xf;
|
|
_host_dir += P3DInstanceManager::encode_hexdigit(high);
|
|
_host_dir += P3DInstanceManager::encode_hexdigit(low);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::read_xhost
|
|
// Access: Private
|
|
// Description: Reads the host data from the <host> (or <alt_host>)
|
|
// entry in the contents.xml file.
|
|
////////////////////////////////////////////////////////////////////
|
|
void P3DHost::
|
|
read_xhost(TiXmlElement *xhost) {
|
|
const char *descriptive_name = xhost->Attribute("descriptive_name");
|
|
if (descriptive_name != NULL && _descriptive_name.empty()) {
|
|
_descriptive_name = descriptive_name;
|
|
}
|
|
|
|
// Get the "download" URL, which is the source from which we
|
|
// download everything other than the contents.xml file.
|
|
const char *download_url = xhost->Attribute("download_url");
|
|
if (download_url != NULL) {
|
|
_download_url_prefix = download_url;
|
|
}
|
|
if (!_download_url_prefix.empty()) {
|
|
if (_download_url_prefix[_download_url_prefix.size() - 1] != '/') {
|
|
_download_url_prefix += "/";
|
|
}
|
|
} else {
|
|
_download_url_prefix = _host_url_prefix;
|
|
}
|
|
|
|
TiXmlElement *xmirror = xhost->FirstChildElement("mirror");
|
|
while (xmirror != NULL) {
|
|
const char *url = xmirror->Attribute("url");
|
|
if (url != NULL) {
|
|
add_mirror(url);
|
|
}
|
|
xmirror = xmirror->NextSiblingElement("mirror");
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::standardize_filename
|
|
// Access: Private, Static
|
|
// Description: Attempts to change the filename into some standard
|
|
// form for comparison with other filenames. On a
|
|
// case-insensitive filesystem, this converts the
|
|
// filename to lowercase. On Windows, it further
|
|
// replaces forward slashes with backslashes.
|
|
////////////////////////////////////////////////////////////////////
|
|
string P3DHost::
|
|
standardize_filename(const string &filename) {
|
|
#if defined(_WIN32) || defined(__APPLE__)
|
|
string new_filename;
|
|
for (string::const_iterator si = filename.begin();
|
|
si != filename.end();
|
|
++si) {
|
|
char ch = *si;
|
|
#ifdef _WIN32
|
|
if (ch == '/') {
|
|
ch = '\\';
|
|
}
|
|
#endif // _WIN32
|
|
new_filename += tolower(ch);
|
|
}
|
|
return new_filename;
|
|
#else // _WIN32 || __APPLE__
|
|
return filename;
|
|
#endif // _WIN32 || __APPLE__
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Function: P3DHost::copy_file
|
|
// Access: Private, Static
|
|
// Description: Copies the data in the file named by from_filename
|
|
// into the file named by to_filename.
|
|
////////////////////////////////////////////////////////////////////
|
|
bool P3DHost::
|
|
copy_file(const string &from_filename, const string &to_filename) {
|
|
ifstream in(from_filename.c_str(), ios::in | ios::binary);
|
|
|
|
// Copy to a temporary file first, in case (a) the filenames
|
|
// actually refer to the same file, or (b) in case we have different
|
|
// processes writing to the same file, and (c) 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();
|
|
ofstream out(temp_filename.c_str(), ios::out | ios::binary);
|
|
|
|
static const size_t buffer_size = 4096;
|
|
char buffer[buffer_size];
|
|
|
|
in.read(buffer, buffer_size);
|
|
size_t count = in.gcount();
|
|
while (count != 0) {
|
|
out.write(buffer, count);
|
|
if (out.fail()) {
|
|
unlink(temp_filename.c_str());
|
|
return false;
|
|
}
|
|
in.read(buffer, buffer_size);
|
|
count = in.gcount();
|
|
}
|
|
out.close();
|
|
|
|
if (!in.eof()) {
|
|
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;
|
|
}
|