Browse Source

lower-impact asynchronous download

David Rose 16 years ago
parent
commit
5f4c4670bd
2 changed files with 144 additions and 82 deletions
  1. 106 55
      direct/src/p3d/PackageInfo.py
  2. 38 27
      direct/src/p3d/PackageInstaller.py

+ 106 - 55
direct/src/p3d/PackageInfo.py

@@ -25,11 +25,12 @@ class PackageInfo:
     unpackFactor = 0.01
     patchFactor = 0.01
 
-    # These tokens are returned by __downloadFile() and other
-    # InstallStep functions.
+    # These tokens are yielded (not returned) by __downloadFile() and
+    # other InstallStep functions.
     stepComplete = 1
     stepFailed = 2
     restartDownload = 3
+    stepContinue = 4
 
     class InstallStep:
         """ This class is one step of the installPlan list; it
@@ -194,29 +195,49 @@ class PackageInfo:
         synchronously, and then reads it.  Returns true on success,
         false on failure. """
 
+        for token in self.downloadDescFileGenerator(http):
+            if token != self.stepContinue:
+                break
+            Thread.considerYield()
+
+        return (token == self.stepComplete)
+    
+    def downloadDescFileGenerator(self, http):
+        """ A generator function that implements downloadDescFile()
+        one piece at a time.  It yields one of stepComplete,
+        stepFailed, or stepContinue. """
+
         assert self.descFile
 
         if self.hasDescFile:
             # We've already got one.
-            return True
+            yield self.stepComplete; return
 
         self.http = http
 
-        token = self.__downloadFile(
+        for token in self.__downloadFile(
             None, self.descFile,
             urlbase = self.descFile.filename,
-            filename = self.descFileBasename)
+            filename = self.descFileBasename):
+            if token == self.stepContinue:
+                yield token
+            else:
+                break
 
         while token == self.restartDownload:
             # Try again.
-            token = self.__downloadFile(
+            for token in self.__downloadFile(
                 None, self.descFile,
                 urlbase = self.descFile.filename,
-                filename = self.descFileBasename)
+                filename = self.descFileBasename):
+                if token == self.stepContinue:
+                    yield token
+                else:
+                    break
 
         if token == self.stepFailed:
             # Couldn't download the desc file.
-            return False
+            yield self.stepFailed; return
 
         assert token == self.stepComplete
 
@@ -228,9 +249,9 @@ class PackageInfo:
             # Weird, it passed the hash check, but we still can't read
             # it.
             self.notify.warning("Failure reading %s" % (filename))
-            return False
+            yield self.stepFailed; return
 
-        return True
+        yield self.stepComplete; return
 
     def __readDescFile(self):
         """ Reads the desc xml file for this particular package,
@@ -500,34 +521,59 @@ class PackageInfo:
         in, which will have been done by self.__readDescFile().
         """
 
+        for token in self.downloadPackageGenerator(http):
+            if token != self.stepContinue:
+                break
+            Thread.considerYield()
+
+        return (token == self.stepComplete)
+    
+    def downloadPackageGenerator(self, http):
+        """ A generator function that implements downloadPackage() one
+        piece at a time.  It yields one of stepComplete, stepFailed,
+        or stepContinue. """
+
         assert self.hasDescFile
 
         if self.hasPackage:
             # We've already got one.
-            return True
+            yield self.stepComplete; return
 
         # We should have an install plan by the time we get here.
         assert self.installPlans
 
         self.http = http
-        token = self.__followInstallPlans()
+        for token in self.__followInstallPlans():
+            if token == self.stepContinue:
+                yield token
+            else:
+                break
+            
         while token == self.restartDownload:
             # Try again.
-            if not self.downloadDescFile(http):
-                return False
-
-            token = self.__followInstallPlans()
+            for token in self.downloadDescFileGenerator(http):
+                if token == self.stepContinue:
+                    yield token
+                else:
+                    break
+            if token == self.stepComplete:
+                for token in self.__followInstallPlans():
+                    if token == self.stepContinue:
+                        yield token
+                    else:
+                        break
 
         if token == self.stepFailed:
-            return False
+            yield self.stepFailed; return
 
         assert token == self.stepComplete
-        return True
+        yield self.stepComplete; return
             
 
     def __followInstallPlans(self):
-        """ Performs all of the steps in self.installPlans.  Returns
-        one of stepComplete, stepFailed, or restartDownload. """
+        """ Performs all of the steps in self.installPlans.  Yields
+        one of stepComplete, stepFailed, restartDownload, or
+        stepContinue. """
 
         if not self.installPlans:
             self.__buildInstallPlans()
@@ -543,9 +589,14 @@ class PackageInfo:
             for step in plan:
                 self.currentStepEffort = step.getEffort()
 
-                token = step.func(step)
+                for token in step.func(step):
+                    if token == self.stepContinue:
+                        yield token
+                    else:
+                        break
+                    
                 if token == self.restartDownload:
-                    return token
+                    yield token
                 if token == self.stepFailed:
                     planFailed = True
                     break
@@ -555,13 +606,13 @@ class PackageInfo:
                 
             if not planFailed:
                 # Successfully downloaded!
-                return self.stepComplete
+                yield self.stepComplete; return
 
             if taskMgr.destroyed:
-                return self.stepFailed
+                yield self.stepFailed; return
 
         # All plans failed.
-        return self.stepFailed
+        yield self.stepFailed; return
 
     def __findPatchChain(self, fileSpec):
         """ Finds the chain of patches that leads from the indicated
@@ -596,8 +647,8 @@ class PackageInfo:
     def __downloadFile(self, step, fileSpec, urlbase = None, filename = None,
                        allowPartial = False):
         """ Downloads the indicated file from the host into
-        packageDir.  Returns one of stepComplete, stepFailed, or
-        restartDownload. """
+        packageDir.  Yields one of stepComplete, stepFailed, 
+        restartDownload, or stepContinue. """
 
         if not urlbase:
             urlbase = self.descFileDirname + '/' + fileSpec.filename
@@ -691,9 +742,9 @@ class PackageInfo:
                     # If the task manager has been destroyed, we must
                     # be shutting down.  Get out of here.
                     self.notify.warning("Task Manager destroyed, aborting %s" % (url))
-                    return self.stepFailed
+                    yield self.stepFailed; return
                     
-                Thread.considerYield()
+                yield self.stepContinue
                 
             if step:
                 step.bytesDone = channel.getBytesDownloaded() + channel.getFirstByteDelivered()
@@ -710,11 +761,11 @@ class PackageInfo:
                 # sure.
                 if self.host.redownloadContentsFile(self.http):
                     # Yes!  Go back and start over from the beginning.
-                    return self.restartDownload
+                    yield self.restartDownload; return
 
             else:
                 # Success!
-                return self.stepComplete
+                yield self.stepComplete; return
 
             # Maybe the mirror is bad.  Go back and try the next
             # mirror.
@@ -723,17 +774,17 @@ class PackageInfo:
         # is stale.  Try re-downloading it now, just to be sure.
         if self.host.redownloadContentsFile(self.http):
             # Yes!  Go back and start over from the beginning.
-            return self.restartDownload
+            yield self.restartDownload; return
 
         # All mirrors failed; the server (or the internet connection)
         # must be just fubar.
-        return self.stepFailed
+        yield self.stepFailed; return
 
     def __applyPatch(self, step, patchfile):
         """ Applies the indicated patching in-place to the current
         uncompressed archive.  The patchfile is removed after the
-        operation.  Returns one of stepComplete, stepFailed, or
-        restartDownload. """
+        operation.  Yields one of stepComplete, stepFailed, 
+        restartDownload, or stepContinue. """
 
         origPathname = Filename(self.getPackageDir(), self.uncompressedArchive.filename)
         patchPathname = Filename(self.getPackageDir(), patchfile.file.filename)
@@ -752,9 +803,9 @@ class PackageInfo:
                 # If the task manager has been destroyed, we must
                 # be shutting down.  Get out of here.
                 self.notify.warning("Task Manager destroyed, aborting patch %s" % (origPathname))
-                return self.stepFailed
+                yield self.stepFailed; return
 
-            Thread.considerYield()
+            yield self.stepContinue
             ret = p.run()
         del p
         patchPathname.unlink()
@@ -762,18 +813,18 @@ class PackageInfo:
         if ret < 0:
             self.notify.warning("Patching of %s failed." % (origPathname))
             result.unlink()
-            return self.stepFailed
+            yield self.stepFailed; return
 
         if not result.renameTo(origPathname):
             self.notify.warning("Couldn't rename %s to %s" % (result, origPathname))
-            return self.stepFailed
+            yield self.stepFailed; return
             
-        return self.stepComplete
+        yield self.stepComplete; return
 
     def __uncompressArchive(self, step):
         """ Turns the compressed archive into the uncompressed
-        archive.  Returns one of stepComplete, stepFailed, or
-        restartDownload. """
+        archive.  Yields one of stepComplete, stepFailed, 
+        restartDownload, or stepContinue. """
 
         sourcePathname = Filename(self.getPackageDir(), self.compressedArchive.filename)
         targetPathname = Filename(self.getPackageDir(), self.uncompressedArchive.filename)
@@ -791,12 +842,12 @@ class PackageInfo:
                 # If the task manager has been destroyed, we must
                 # be shutting down.  Get out of here.
                 self.notify.warning("Task Manager destroyed, aborting decompresss %s" % (sourcePathname))
-                return self.stepFailed
+                yield self.stepFailed; return
 
-            Thread.considerYield()
+            yield self.stepContinue
 
         if result != EUSuccess:
-            return self.stepFailed
+            yield self.stepFailed; return
             
         step.bytesDone = totalBytes
         self.__updateStepProgress(step)
@@ -804,31 +855,31 @@ class PackageInfo:
         if not self.uncompressedArchive.quickVerify(self.getPackageDir(), notify= self.notify):
             self.notify.warning("after uncompressing, %s still incorrect" % (
                 self.uncompressedArchive.filename))
-            return self.stepFailed
+            yield self.stepFailed; return
 
         # Now that we've verified the archive, make it read-only.
         os.chmod(targetPathname.toOsSpecific(), 0444)
 
         # Now we can safely remove the compressed archive.
         sourcePathname.unlink()
-        return self.stepComplete
+        yield self.stepComplete; return
     
     def __unpackArchive(self, step):
         """ Unpacks any files in the archive that want to be unpacked
-        to disk.  Returns one of stepComplete, stepFailed, or
-        restartDownload. """
+        to disk.  Yields one of stepComplete, stepFailed, 
+        restartDownload, or stepContinue. """
 
         if not self.extracts:
             # Nothing to extract.
             self.hasPackage = True
-            return self.stepComplete
+            yield self.stepComplete; return
 
         mfPathname = Filename(self.getPackageDir(), self.uncompressedArchive.filename)
         self.notify.info("Unpacking %s" % (mfPathname))
         mf = Multifile()
         if not mf.openRead(mfPathname):
             self.notify.warning("Couldn't open %s" % (mfPathname))
-            return self.stepFailed
+            yield self.stepFailed; return
         
         allExtractsOk = True
         step.bytesDone = 0
@@ -860,15 +911,15 @@ class PackageInfo:
                 # If the task manager has been destroyed, we must
                 # be shutting down.  Get out of here.
                 self.notify.warning("Task Manager destroyed, aborting unpacking %s" % (mfPathname))
-                return self.stepFailed
+                yield self.stepFailed; return
 
-            Thread.considerYield()
+            yield self.stepContinue
 
         if not allExtractsOk:
-            return self.stepFailed
+            yield self.stepFailed; return
 
         self.hasPackage = True
-        return self.stepComplete
+        yield self.stepComplete; return
 
     def installPackage(self, appRunner):
         """ Mounts the package and sets up system paths so it becomes

+ 38 - 27
direct/src/p3d/PackageInstaller.py

@@ -1,5 +1,5 @@
 from direct.showbase.DirectObject import DirectObject
-from direct.stdpy.threading import Lock
+from direct.stdpy.threading import Lock, RLock
 from direct.showbase.MessengerGlobal import messenger
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.p3d.PackageInfo import PackageInfo
@@ -142,7 +142,7 @@ class PackageInstaller(DirectObject):
 
             return True
 
-    def __init__(self, appRunner, taskChain = 'install'):
+    def __init__(self, appRunner, taskChain = 'default'):
         self.globalLock.acquire()
         try:
             self.uniqueId = PackageInstaller.nextUniqueId
@@ -153,9 +153,10 @@ class PackageInstaller(DirectObject):
         self.appRunner = appRunner
         self.taskChain = taskChain
         
-        # If the task chain hasn't yet been set up, create the
+        # If we're to be running on an asynchronous task chain, and
+        # the task chain hasn't yet been set up already, create the
         # default parameters now.
-        if not taskMgr.hasTaskChain(self.taskChain):
+        if taskChain != 'default' and not taskMgr.hasTaskChain(self.taskChain):
             taskMgr.setupTaskChain(self.taskChain, numThreads = 1,
                                    threadPriority = TPLow)
 
@@ -165,7 +166,7 @@ class PackageInstaller(DirectObject):
 
         # A list of all packages that have been added to the
         # installer.
-        self.packageLock = Lock()
+        self.packageLock = RLock()
         self.packages = []
         self.state = self.S_initial
 
@@ -527,33 +528,43 @@ class PackageInstaller(DirectObject):
         """ This task runs on the aysynchronous task chain; each pass,
         it extracts one package from self.needsDownload and downloads
         it. """
-        
-        self.packageLock.acquire()
-        try:
-            # If we're done downloading, stop the task.
-            if self.state == self.S_done or not self.needsDownload:
-                self.downloadTask = None
-                return task.done
 
-            assert self.state == self.S_started        
-            pp = self.needsDownload[0]
-            del self.needsDownload[0]
-        finally:
+        while True:
+            self.packageLock.acquire()
+            try:
+                # If we're done downloading, stop the task.
+                if self.state == self.S_done or not self.needsDownload:
+                    self.downloadTask = None
+                    self.packageLock.release()
+                    yield task.done; return
+
+                assert self.state == self.S_started        
+                pp = self.needsDownload[0]
+                del self.needsDownload[0]
+            except:
+                self.packageLock.release()
             self.packageLock.release()
 
-        # Now serve this one package.
-        messenger.send('PackageInstaller-%s-packageStarted' % self.uniqueId,
-                       [pp], taskChain = 'default')
+            # Now serve this one package.
+            messenger.send('PackageInstaller-%s-packageStarted' % self.uniqueId,
+                           [pp], taskChain = 'default')
 
-        if not pp.package.hasPackage:
-            if not pp.package.downloadPackage(self.appRunner.http):
-                self.__donePackage(pp, False)
-                return task.cont
+            if not pp.package.hasPackage:
+                for token in pp.package.downloadPackageGenerator(self.appRunner.http):
+                    if token == pp.package.stepContinue:
+                        yield task.cont
+                    else:
+                        break
 
-        # Successfully downloaded and installed.
-        self.__donePackage(pp, True)
-        
-        return task.cont
+                if token != pp.package.stepComplete:
+                    self.__donePackage(pp, False)
+                    yield task.cont
+                    continue
+
+            # Successfully downloaded and installed.
+            self.__donePackage(pp, True)
+
+            yield task.cont
         
     def __donePackage(self, pp, success):
         """ Marks the indicated package as done, either successfully