diff --git a/direct/src/p3d/DWBPackageInstaller.py b/direct/src/p3d/DWBPackageInstaller.py index b0db053e63..c1df7a1bc8 100644 --- a/direct/src/p3d/DWBPackageInstaller.py +++ b/direct/src/p3d/DWBPackageInstaller.py @@ -52,7 +52,7 @@ class DWBPackageInstaller(DirectWaitBar, PackageInstaller): downloadStarted() and downloadFinished() to indicate the start of a new package. """ if self['updateText']: - self['text'] = 'Installing %s' % (package.displayName) + self['text'] = 'Installing %s' % (package.getFormattedName()) self.show() def packageProgress(self, package, progress): diff --git a/direct/src/p3d/HostInfo.py b/direct/src/p3d/HostInfo.py index bbf7b61901..cd662754f1 100644 --- a/direct/src/p3d/HostInfo.py +++ b/direct/src/p3d/HostInfo.py @@ -25,7 +25,8 @@ class HostInfo: # contents file is read. self.descriptiveName = None - # A list of known mirrors for this host. + # A list of known mirrors for this host, all URL's guaranteed + # to end with a slash. self.mirrors = [] # A map of keyword -> altHost URL's. An altHost is different @@ -159,8 +160,11 @@ class HostInfo: xmirror = xhost.FirstChildElement('mirror') while xmirror: url = xmirror.Attribute('url') - if url and url not in self.mirrors: - self.mirrors.append(url) + if url: + if url[-1] != '/': + url += '/' + if url not in self.mirrors: + self.mirrors.append(url) xmirror = xmirror.NextSiblingElement('mirror') xalthost = xhost.FirstChildElement('alt_host') diff --git a/direct/src/p3d/PackageInfo.py b/direct/src/p3d/PackageInfo.py index b39bac2c09..1229b981bb 100644 --- a/direct/src/p3d/PackageInfo.py +++ b/direct/src/p3d/PackageInfo.py @@ -3,6 +3,7 @@ from direct.p3d.FileSpec import FileSpec from direct.showbase import VFSImporter import os import sys +import random class PackageInfo: @@ -52,12 +53,12 @@ class PackageInfo: # These will be filled in by HostInfo when the package is read # from contents.xml. - self.descFileUrl = None self.descFile = None self.importDescFile = None # These are filled in when the desc file is successfully read. self.hasDescFile = False + self.patchVersion = None self.displayName = None self.uncompressedArchive = None self.compressedArchive = None @@ -90,14 +91,32 @@ class PackageInfo: return size + def getFormattedName(self): + """ Returns the name of this package, for output to the user. + This will be the "public" name of the package, as formatted + for user consumption; it will include capital letters and + spaces where appropriate. """ + + if self.displayName: + name = self.displayName + else: + name = self.packageName + if self.packageVersion: + name += ' %s' % (self.packageVersion) + + if self.patchVersion: + name += ' rev %s' % (self.patchVersion) + + return name + + def setupFilenames(self): """ This is called by the HostInfo when the package is read from contents.xml, to set up the internal filenames and such that rely on some of the information from contents.xml. """ - self.descFileUrl = self.host.hostUrlPrefix + self.descFile.filename - - basename = self.descFile.filename.rsplit('/', 1)[-1] + dirname, basename = self.descFile.filename.rsplit('/', 1) + self.descFileDirname = dirname self.descFileBasename = basename def checkStatus(self): @@ -136,28 +155,21 @@ class PackageInfo: # We've already got one. return True - url = URLSpec(self.descFileUrl) - print "Downloading desc file %s" % (url) - - rf = Ramfile() - channel = http.getDocument(url) - if not channel.downloadToRam(rf): - print "Unable to download %s" % (url) + self.http = http + if not self.__downloadFile( + None, self.descFile, + urlbase = self.descFile.filename, + filename = self.descFileBasename): + # Couldn't download the desc file. return False filename = Filename(self.packageDir, self.descFileBasename) - filename.makeDir() - filename.unlink() - f = open(filename.toOsSpecific(), 'wb') - f.write(rf.getData()) - f.close() - # Now that we've written the desc file, make it read-only. os.chmod(filename.toOsSpecific(), 0444) - try: - self.readDescFile() - except ValueError: + if not self.readDescFile(): + # Weird, it passed the hash check, but we still can't read + # it. print "Failure reading %s" % (filename) return False @@ -165,24 +177,28 @@ class PackageInfo: def readDescFile(self): """ Reads the desc xml file for this particular package. - Presumably this has already been downloaded and installed. """ + Returns true on success, false on failure. """ if self.hasDescFile: # No need to read it again. - return + return True filename = Filename(self.packageDir, self.descFileBasename) doc = TiXmlDocument(filename.toOsSpecific()) if not doc.LoadFile(): - raise ValueError + return False xpackage = doc.FirstChildElement('package') if not xpackage: - raise ValueError + return False + + try: + self.patchVersion = int(xpackage.Attribute('patch_version') or '') + except ValueError: + self.patchVersion = None self.displayName = None - xconfig = xpackage.FirstChildElement('config') if xconfig: # The name for display to an English-speaking user. @@ -216,9 +232,9 @@ class PackageInfo: if self.__checkArchiveStatus(): # It's all good. self.hasPackage = True - return + return True - # We need to download an update. + # Now set up to download the update. self.hasPackage = False # Now determine what we will need to download, and build a @@ -238,7 +254,7 @@ class PackageInfo: self.uncompressedArchive.actualFile = None if self.uncompressedArchive.quickVerify(self.packageDir): self.installPlans = [planA] - return + return True # Maybe the compressed archive file is good. if self.compressedArchive.quickVerify(self.packageDir): @@ -246,11 +262,11 @@ class PackageInfo: step = self.InstallStep(self.__uncompressArchive, uncompressSize, self.uncompressFactor) planA = [step] + planA self.installPlans = [planA] - return + return True # Maybe we can download one or more patches. We'll come back - # to that in a minute as plan A. For now, construct on plan - # B, which will be to download the whole archive. + # to that in a minute as plan A. For now, construct plan B, + # which will be to download the whole archive. planB = planA[:] uncompressSize = self.uncompressedArchive.size @@ -285,6 +301,8 @@ class PackageInfo: # plan B as the only plan. self.installPlans = [planB] + return True + def __scanDirectoryRecursively(self, dirname): """ Generates a list of Filename objects: all of the files (not directories) within and below the indicated dirname. """ @@ -431,39 +449,66 @@ class PackageInfo: patchMaker.cleanup() return plan - def __downloadFile(self, step, fileSpec): + def __downloadFile(self, step, fileSpec, urlbase = None, filename = None): """ Downloads the indicated file from the host into packageDir. Returns true on success, false on failure. """ - - url = self.descFileUrl.rsplit('/', 1)[0] - url += '/' + fileSpec.filename - url = DocumentSpec(url) - print "Downloading package file %s" % (url) - targetPathname = Filename(self.packageDir, fileSpec.filename) - targetPathname.setBinary() - - channel = self.http.makeChannel(False) - # TODO: check for a previous partial download, and resume it. - targetPathname.unlink() - channel.beginGetDocument(url) - channel.downloadToFile(targetPathname) - while channel.run(): - step.bytesDone = channel.getBytesDownloaded() - self.__updateStepProgress(step) - Thread.considerYield() - step.bytesDone = channel.getBytesDownloaded() - self.__updateStepProgress(step) - if not channel.isValid(): - print "Failed to download %s" % (url) - return False + if not urlbase: + urlbase = self.descFileDirname + '/' + fileSpec.filename - if not fileSpec.fullVerify(self.packageDir): - print "after downloading, %s incorrect" % ( - fileSpec.filename) - return False - - return True + # Build up a list of URL's to try downloading from. + tryUrls = [] + if self.host.mirrors: + # Choose a mirror at random first, then a different + # mirror. + mirrors = self.host.mirrors[:] + for i in range(2): + mirror = random.choice(mirrors) + mirrors.remove(mirror) + tryUrls.append(mirror + urlbase) + if not mirrors: + break + + # After trying two mirrors and failing (or if there are no + # mirrors), go get it from the original host. + url = self.host.hostUrlPrefix + urlbase + tryUrls.append(url) + + for url in tryUrls: + url = DocumentSpec(url) + print "Downloading package file %s" % (url) + + if not filename: + filename = fileSpec.filename + targetPathname = Filename(self.packageDir, filename) + targetPathname.setBinary() + + channel = self.http.makeChannel(False) + # TODO: check for a previous partial download, and resume it. + targetPathname.makeDir() + targetPathname.unlink() + channel.beginGetDocument(url) + channel.downloadToFile(targetPathname) + while channel.run(): + if step: + step.bytesDone = channel.getBytesDownloaded() + self.__updateStepProgress(step) + Thread.considerYield() + if step: + step.bytesDone = channel.getBytesDownloaded() + self.__updateStepProgress(step) + if not channel.isValid(): + print "Failed to download %s" % (url) + continue + + if not fileSpec.fullVerify(self.packageDir, pathname = targetPathname): + print "After downloading, %s incorrect" % (url) + continue + + return True + + # All mirrors failed. + return False def __applyPatch(self, step, patchfile): """ Applies the indicated patching in-place to the current diff --git a/direct/src/p3d/PatchMaker.py b/direct/src/p3d/PatchMaker.py index 06daf86474..8ed2c76095 100644 --- a/direct/src/p3d/PatchMaker.py +++ b/direct/src/p3d/PatchMaker.py @@ -247,10 +247,11 @@ class PatchMaker: """ This is a particular package. This contains all of the information needed to reconstruct the package's desc file. """ - def __init__(self, packageDesc, patchMaker): + def __init__(self, packageDesc, patchMaker, xpackage): self.packageDir = Filename(patchMaker.installDir, packageDesc.getDirname()) self.packageDesc = packageDesc self.patchMaker = patchMaker + self.contentsDocPackage = xpackage self.patchVersion = 1 self.currentPv = None self.basePv = None @@ -384,9 +385,9 @@ class PatchMaker: """ Rewrites the desc file with the new patch information. """ - if not self.anyChanges: - # No need to rewrite. - return +## if not self.anyChanges: +## # No need to rewrite. +## return xpackage = self.doc.FirstChildElement('package') if not xpackage: @@ -418,6 +419,13 @@ class PatchMaker: xpackage.InsertEndChild(xpatch) self.doc.SaveFile() + + # Now that we've rewritten the xml file, we have to change + # the contents.xml file that references it to indicate the + # new file hash. + fileSpec = FileSpec() + fileSpec.fromFile(self.patchMaker.installDir, self.packageDesc) + fileSpec.storeXml(self.contentsDocPackage) def __init__(self, installDir): @@ -440,6 +448,7 @@ class PatchMaker: else: self.processSomePackages(packageNames) + self.writeContentsFile() self.cleanup() return True @@ -481,14 +490,25 @@ class PatchMaker: filename = xpackage.Attribute('filename') if filename and not solo: filename = Filename(filename) - package = self.Package(filename, self) + package = self.Package(filename, self, xpackage) package.readDescFile() self.packages.append(package) xpackage = xpackage.NextSiblingElement('package') + self.contentsDoc = doc + return True + def writeContentsFile(self): + """ Writes the contents.xml file at the end of processing. """ + + # We trust each of the packages to have already updated their + # element within the contents.xml document, so all we have to + # do is write out the document. + + self.contentsDoc.SaveFile() + def getPackageVersion(self, key): """ Returns a shared PackageVersion object for the indicated key. """ diff --git a/direct/src/plugin/p3dInstance.cxx b/direct/src/plugin/p3dInstance.cxx index 5e5a997114..59396fc35e 100644 --- a/direct/src/plugin/p3dInstance.cxx +++ b/direct/src/plugin/p3dInstance.cxx @@ -1825,10 +1825,7 @@ start_next_download() { if (!package->get_ready()) { // This package is ready to download. Begin. - string name = package->get_package_display_name(); - if (name.empty()) { - name = package->get_package_name(); - } + string name = package->get_formatted_name(); _panda_script_object->set_string_property("downloadPackageName", package->get_package_name()); _panda_script_object->set_string_property("downloadPackageDisplayName", name); _panda_script_object->set_int_property("downloadPackageNumber", _download_package_index + 1); diff --git a/direct/src/plugin/p3dPackage.I b/direct/src/plugin/p3dPackage.I index 7bb408bfa1..a113e9c480 100755 --- a/direct/src/plugin/p3dPackage.I +++ b/direct/src/plugin/p3dPackage.I @@ -113,10 +113,8 @@ get_package_version() const { //////////////////////////////////////////////////////////////////// // Function: P3DPackage::get_package_display_name // Access: Public -// Description: Returns the name of this package, for output to the -// user. This will be the "public" name of the package, -// as formatted for user consumption; it will include -// capital letters and spaces where appropriate. +// Description: Returns the display_name name of this package, as set +// in the desc file. //////////////////////////////////////////////////////////////////// inline const string &P3DPackage:: get_package_display_name() const { diff --git a/direct/src/plugin/p3dPackage.cxx b/direct/src/plugin/p3dPackage.cxx index a4c035ea04..dc80e6c25c 100755 --- a/direct/src/plugin/p3dPackage.cxx +++ b/direct/src/plugin/p3dPackage.cxx @@ -51,6 +51,7 @@ P3DPackage(P3DHost *host, const string &package_name, if (!_package_version.empty()) { _package_fullname += string(".") + _package_version; } + _patch_version = 0; // This is set true if the package is a "solo", i.e. a single // file, instead of an xml file and a multifile to unpack. @@ -130,6 +131,34 @@ activate_download() { } } +//////////////////////////////////////////////////////////////////// +// Function: P3DPackage::get_formatted_name +// Access: Public +// Description: Returns the name of this package, for output to the +// user. This will be the "public" name of the package, +// as formatted for user consumption; it will include +// capital letters and spaces where appropriate. +//////////////////////////////////////////////////////////////////// +string P3DPackage:: +get_formatted_name() const { + ostringstream strm; + + if (!_package_display_name.empty()) { + strm << _package_display_name; + } else { + strm << _package_name; + if (!_package_version.empty()) { + strm << " " << _package_version; + } + } + + if (_patch_version != 0) { + strm << " rev " << _patch_version; + } + + return strm.str(); +} + //////////////////////////////////////////////////////////////////// // Function: P3DPackage::add_instance // Access: Public @@ -442,6 +471,8 @@ got_desc_file(TiXmlDocument *doc, bool freshly_downloaded) { TiXmlElement *xcompressed_archive = NULL; if (xpackage != NULL) { + xpackage->Attribute("patch_version", &_patch_version); + xuncompressed_archive = xpackage->FirstChildElement("uncompressed_archive"); xcompressed_archive = xpackage->FirstChildElement("compressed_archive"); diff --git a/direct/src/plugin/p3dPackage.h b/direct/src/plugin/p3dPackage.h index 84f2244387..8b1e744d91 100755 --- a/direct/src/plugin/p3dPackage.h +++ b/direct/src/plugin/p3dPackage.h @@ -57,6 +57,7 @@ public: inline const string &get_package_name() const; inline const string &get_package_version() const; inline const string &get_package_display_name() const; + string get_formatted_name() const; inline const TiXmlElement *get_xconfig() const; inline const string &get_desc_file_pathname() const; @@ -118,6 +119,7 @@ private: string _package_name; string _package_version; string _package_platform; + int _patch_version; string _alt_host; bool _package_solo; string _package_display_name;