Просмотр исходного кода

PackageInstaller, runtime package installation

David Rose 16 лет назад
Родитель
Сommit
81363c4ba9

+ 3 - 4
direct/src/gui/DirectWaitBar.py

@@ -13,10 +13,9 @@ d = DirectWaitBar(borderWidth=(0, 0))
 """
 
 class DirectWaitBar(DirectFrame):
-    """
-    DirectEntry(parent) - Create a DirectGuiWidget which responds
-    to keyboard buttons
-    """
+    """ DirectWaitBar - A DirectWidget that shows progress completed
+    towards a task.  """
+    
     def __init__(self, parent = None, **kw):
         # Inherits from DirectFrame
         # A Direct Frame can have:

+ 89 - 64
direct/src/p3d/AppRunner.py

@@ -36,6 +36,16 @@ class ScriptAttributes:
     pass
 
 class AppRunner(DirectObject):
+
+    """ This class is intended to be compiled into the Panda3D runtime
+    distributable, to execute a packaged p3d application.  It also
+    provides some useful runtime services while running in that
+    packaged environment.
+
+    It does not usually exist while running Python directly, but you
+    can use dummyAppRunner() to create one at startup for testing or
+    development purposes.  """
+    
     def __init__(self):
         DirectObject.__init__(self)
 
@@ -45,6 +55,9 @@ class AppRunner(DirectObject):
         # child.
         sys.stdout = sys.stderr
 
+        # This is set true by dummyAppRunner(), below.
+        self.dummy = False
+
         self.sessionId = 0
         self.packedAppEnvironmentInitialized = False
         self.gotWindow = False
@@ -54,8 +67,6 @@ class AppRunner(DirectObject):
         self.windowPrc = None
         self.http = HTTPClient.getGlobalPtr()
 
-        self.fullDiskAccess = False
-
         self.Undefined = Undefined
         self.ConcreteStruct = ConcreteStruct
 
@@ -72,10 +83,18 @@ class AppRunner(DirectObject):
         # A list of the Panda3D packages that have been loaded.
         self.installedPackages = []
 
+        # A list of the Panda3D packages that in the queue to be
+        # downloaded.
+        self.downloadingPackages = []
+
         # A dictionary of HostInfo objects for the various download
         # hosts we have imported packages from.
         self.hosts = {}
 
+        # Managing packages for runtime download.
+        self.downloadingPackages = []
+        self.downloadTask = None
+
         # The mount point for the multifile.  For now, this is always
         # the same, but when we move to multiple-instance sessions, it
         # may have to be different for each instance.
@@ -116,6 +135,36 @@ class AppRunner(DirectObject):
         # call back to the main thread.
         self.accept('AppRunner_startIfReady', self.__startIfReady)
 
+    def installPackage(self, packageName, version = None, hostUrl = None):
+
+        """ Installs the named package, downloading it first if
+        necessary.  Returns true on success, false on failure.  This
+        method runs synchronously, and will block until it is
+        finished; see the PackageInstaller class if you want this to
+        happen asynchronously instead. """
+
+        host = self.getHost(hostUrl)
+        if not host.downloadContentsFile(self.http):
+            return False
+
+        # All right, get the package info now.
+        package = host.getPackage(packageName, version)
+        if not package:
+            print "Package %s %s not known on %s" % (
+                packageName, version, hostUrl)
+            return False
+
+        if not package.downloadDescFile(self.http):
+            return False
+
+        if not package.downloadPackage(self.http):
+            return False
+
+        if not package.installPackage(self):
+            return False
+
+        print "Package %s %s installed." % (packageName, version)
+
     def getHost(self, hostUrl):
         """ Returns a new HostInfo object corresponding to the
         indicated host URL.  If we have already seen this URL
@@ -163,7 +212,7 @@ class AppRunner(DirectObject):
             return False
 
         return True
-            
+
     def stop(self):
         """ This method can be called by JavaScript to stop the
         application. """
@@ -191,40 +240,6 @@ class AppRunner(DirectObject):
 
         vfs = VirtualFileSystem.getGlobalPtr()
 
-        # Unmount directories we don't need.  This doesn't provide
-        # actual security, since it only disables this stuff for users
-        # who go through the vfs; a malicious programmer can always
-        # get to the underlying true file I/O operations.  Still, it
-        # can help prevent honest developers from accidentally getting
-        # stuck where they don't belong.
-        if not self.fullDiskAccess:
-            # Clear *all* the mount points, including "/", so that we
-            # no longer access the disk directly.
-            vfs.unmountAll()
-
-            # Make sure the directories on our standard Python path
-            # are mounted read-only, so we can still load Python.
-            # Note: read-only actually doesn't have any effect on the
-            # vfs right now; careless application code can still write
-            # to these directories inadvertently.
-            for dirname in sys.path:
-                dirname = Filename.fromOsSpecific(dirname)
-                if dirname.isDirectory():
-                    vfs.mount(dirname, dirname, vfs.MFReadOnly)
-
-            # Also mount some standard directories read-write
-            # (temporary and app-data directories).
-            tdir = Filename.temporary('', '')
-            for dirname in set([ tdir.getDirname(),
-                                 Filename.getTempDirectory().cStr(),
-                                 Filename.getUserAppdataDirectory().cStr(),
-                                 Filename.getCommonAppdataDirectory().cStr() ]):
-                vfs.mount(dirname, dirname, 0)
-
-            # And we might need the current working directory.
-            dirname = ExecutionEnvironment.getCwd()
-            vfs.mount(dirname, dirname, 0)
-
         # Now set up Python to import this stuff.
         VFSImporter.register()
         sys.path = [ self.multifileRoot ] + sys.path
@@ -240,11 +255,6 @@ class AppRunner(DirectObject):
         os.listdir = file.listdir
         os.walk = file.walk
 
-        if not self.fullDiskAccess:
-            # Make "/mf" our "current directory", for running the multifiles
-            # we plan to mount there.
-            vfs.chdir(self.multifileRoot)
-
     def __startIfReady(self):
         """ Called internally to start the application. """
         if self.started:
@@ -320,7 +330,12 @@ class AppRunner(DirectObject):
         application. """
 
         host = self.getHost(hostUrl)
-        host.readContentsFile()
+
+        try:
+            host.readContentsFile()
+        except ValueError:
+            print "Host %s has not been downloaded, cannot preload %s." % (hostUrl, name)
+            return
 
         if not platform:
             platform = None
@@ -328,6 +343,14 @@ class AppRunner(DirectObject):
         assert package
         self.installedPackages.append(package)
 
+        if package.checkStatus():
+            # The package should have been loaded already.  If it has,
+            # go ahead and mount it.
+            package.installPackage(self)
+        else:
+            print "%s %s is not preloaded." % (
+                package.packageName, package.packageVersion)
+
     def setP3DFilename(self, p3dFilename, tokens = [], argv = [],
                        instanceId = None):
         """ Called by the browser to specify the p3d file that
@@ -375,36 +398,36 @@ class AppRunner(DirectObject):
         if self.p3dInfo:
             self.p3dPackage = self.p3dInfo.FirstChildElement('package')
 
-        if self.p3dPackage:
-            fullDiskAccess = self.p3dPackage.Attribute('full_disk_access')
-            try:
-                self.fullDiskAccess = int(fullDiskAccess or '')
-            except ValueError:
-                pass
-
         self.initPackedAppEnvironment()
 
         # Mount the Multifile under /mf, by convention.
         vfs.mount(mf, self.multifileRoot, vfs.MFReadOnly)
         VFSImporter.freeze_new_modules(mf, self.multifileRoot)
 
-        # Load any prc files in the root.  We have to load them
-        # explicitly, since the ConfigPageManager can't directly look
-        # inside the vfs.  Use the Multifile interface to find the prc
-        # files, rather than vfs.scanDirectory(), so we only pick up the
-        # files in this particular multifile.
-        for f in mf.getSubfileNames():
-            fn = Filename(f)
-            if fn.getDirname() == '' and fn.getExtension() == 'prc':
-                pathname = '%s/%s' % (self.multifileRoot, f)
-                data = open(pathname, 'r').read()
-                loadPrcFileData(pathname, data)
-
+        self.loadMultifilePrcFiles(mf, self.multifileRoot)
         self.gotP3DFilename = True
 
         # Send this call to the main thread; don't call it directly.
         messenger.send('AppRunner_startIfReady', taskChain = 'default')
 
+    def loadMultifilePrcFiles(self, mf, root):
+        """ Loads any prc files in the root of the indicated
+        Multifile, which is presumbed to have been mounted already
+        under root. """
+        
+        # We have to load these prc files explicitly, since the
+        # ConfigPageManager can't directly look inside the vfs.  Use
+        # the Multifile interface to find the prc files, rather than
+        # vfs.scanDirectory(), so we only pick up the files in this
+        # particular multifile.
+        for f in mf.getSubfileNames():
+            fn = Filename(f)
+            if fn.getDirname() == '' and fn.getExtension() == 'prc':
+                pathname = '%s/%s' % (root, f)
+                data = open(pathname, 'r').read()
+                loadPrcFileData(pathname, data)
+        
+    
     def __clearWindowPrc(self):
         """ Clears the windowPrc file that was created in a previous
         call to setupWindow(), if any. """
@@ -563,7 +586,7 @@ class AppRunner(DirectObject):
 
         self.sendRequest('drop_p3dobj', objectId)
 
-def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
+def dummyAppRunner(tokens = [], argv = None):
     """ This function creates a dummy global AppRunner object, which
     is useful for testing running in a packaged environment without
     actually bothering to package up the application.  Call this at
@@ -580,6 +603,7 @@ def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
         return
 
     appRunner = AppRunner()
+    appRunner.dummy = True
     AppRunnerGlobal.appRunner = appRunner
 
     platform = PandaSystem.getPlatform()
@@ -604,7 +628,6 @@ def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
 
     appRunner.p3dInfo = None
     appRunner.p3dPackage = None
-    appRunner.fullDiskAccess = fullDiskAccess
 
     # Mount the current directory under the multifileRoot, as if it
     # were coming from a multifile.
@@ -619,3 +642,5 @@ def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
     os.listdir = file.listdir
     os.walk = file.walk
 
+    return appRunner
+

+ 64 - 0
direct/src/p3d/DWBPackageInstaller.py

@@ -0,0 +1,64 @@
+from direct.p3d.PackageInstaller import PackageInstaller
+from direct.gui.DirectWaitBar import DirectWaitBar
+from direct.gui import DirectGuiGlobals as DGG
+
+class DWBPackageInstaller(DirectWaitBar, PackageInstaller):
+    """ This class presents a PackageInstaller that also inherits from
+    DirectWaitBar, so it updates its own GUI as it downloads. """
+
+    def __init__(self, appRunner, parent = None, **kw):
+        PackageInstaller.__init__(self, appRunner)
+
+        optiondefs = (
+            ('borderWidth',    (0.01, 0.01),       None),
+            ('relief',         DGG.SUNKEN,         self.setRelief),
+            ('range',          1,                  self.setRange),
+            ('barBorderWidth', (0.01, 0.01),       self.setBarBorderWidth),
+            ('barColor',       (0.424, 0.647, 0.878, 1),  self.setBarColor),
+            ('barRelief',      DGG.RAISED,         self.setBarRelief),
+            ('text',           'Starting',         self.setText),
+            ('text_pos',       (0, -0.025),        None),
+            ('text_scale',     0.1,                None)
+            )
+        self.defineoptions(kw, optiondefs)
+        DirectWaitBar.__init__(self, parent, **kw)
+        self.initialiseoptions(DWBPackageInstaller)
+        self.updateBarStyle()
+
+        # Hidden by default until the download begins.
+        self.hide()
+
+    def cleanup(self):
+        PackageInstaller.cleanup(self)
+        DirectWaitBar.destroy(self)
+
+    def destroy(self):
+        PackageInstaller.cleanup(self)
+        DirectWaitBar.destroy(self)
+
+    def packageStarted(self, package):
+        """ This callback is made for each package between
+        downloadStarted() and downloadFinished() to indicate the start
+        of a new package. """
+        self['text'] = 'Installing %s' % (package.displayName)
+        self.show()
+        
+    def downloadProgress(self, overallProgress):
+        """ This callback is made repeatedly between downloadStarted()
+        and downloadFinished() to update the current progress through
+        all packages.  The progress value ranges from 0 (beginning) to
+        1 (complete). """
+
+        self['value'] = overallProgress * self['range']
+
+    def downloadFinished(self, success):
+        """ This callback is made when all of the packages have been
+        downloaded and installed (or there has been some failure).  If
+        all packages where successfully installed, success is True.
+
+        If there were no packages that required downloading, this
+        callback will be made immediately, *without* a corresponding
+        call to downloadStarted(). """
+
+        self.hide()
+        

+ 1 - 1
direct/src/p3d/FileSpec.py

@@ -113,7 +113,7 @@ class FileSpec:
         redownloaded. """
 
         if not pathname:
-            pathname = Filename(packageDir, pathname)
+            pathname = Filename(packageDir, self.filename)
         try:
             st = os.stat(pathname.toOsSpecific())
         except OSError:

+ 36 - 3
direct/src/p3d/HostInfo.py

@@ -1,4 +1,4 @@
-from pandac.PandaModules import TiXmlDocument, HashVal, Filename, PandaSystem
+from pandac.PandaModules import TiXmlDocument, HashVal, Filename, PandaSystem, URLSpec, Ramfile
 from direct.p3d.PackageInfo import PackageInfo
 from direct.p3d.FileSpec import FileSpec
 
@@ -31,6 +31,37 @@ class HostInfo:
         self.__determineHostDir(appRunner)
         self.importsDir = Filename(self.hostDir, 'imports')
 
+    def downloadContentsFile(self, http):
+        """ Downloads the contents.xml file for this particular host,
+        synchronously, and then reads it.  Returns true on success,
+        false on failure. """
+
+        if self.hasContentsFile:
+            # We've already got one.
+            return True
+
+        url = URLSpec(self.hostUrlPrefix + 'contents.xml')
+        print "Downloading %s" % (url)
+
+        rf = Ramfile()
+        channel = http.getDocument(url)
+        if not channel.downloadToRam(rf):
+            print "Unable to download %s" % (url)
+
+        filename = Filename(self.hostDir, 'contents.xml')
+        filename.makeDir()
+        f = open(filename.toOsSpecific(), 'wb')
+        f.write(rf.getData())
+        f.close()
+
+        try:
+            self.readContentsFile()
+        except ValueError:
+            print "Failure reading %s" % (filename)
+            return False
+
+        return True
+
     def readContentsFile(self):
         """ Reads the contents.xml file for this particular host.
         Presumably this has already been downloaded and installed. """
@@ -43,7 +74,7 @@ class HostInfo:
 
         doc = TiXmlDocument(filename.toOsSpecific())
         if not doc.LoadFile():
-            raise IOError
+            raise ValueError
 
         xcontents = doc.FirstChildElement('contents')
         if not xcontents:
@@ -60,6 +91,7 @@ class HostInfo:
             package = self.__makePackage(name, platform, version)
             package.descFile = FileSpec()
             package.descFile.loadXml(xpackage)
+            package.setupFilenames()
 
             xpackage = xpackage.NextSiblingElement('package')
 
@@ -100,6 +132,7 @@ class HostInfo:
         version and the indicated platform or the current runtime
         platform, if one is provided by this host, or None if not. """
 
+        assert self.hasContentsFile
         platforms = self.packages.get((name, version), {})
 
         if platform is not None:
@@ -115,7 +148,7 @@ class HostInfo:
         # If not found, look for one matching no particular platform.
         if not package:
             package = platforms.get(None, None)
-            
+
         return package
 
     def __determineHostDir(self, appRunner):

+ 329 - 4
direct/src/p3d/PackageInfo.py

@@ -1,4 +1,7 @@
-from pandac.PandaModules import Filename
+from pandac.PandaModules import Filename, URLSpec, DocumentSpec, Ramfile, TiXmlDocument, Multifile, Decompressor, EUOk, EUSuccess, VirtualFileSystem, Thread
+from direct.p3d.FileSpec import FileSpec
+import os
+import sys
 
 class PackageInfo:
 
@@ -12,11 +15,333 @@ class PackageInfo:
         self.packageVersion = packageVersion
         self.platform = platform
 
-        self.packageFullname = '%s.%s' % (self.packageName, self.packageVersion)
-        self.packageDir = Filename(host.hostDir, 'packages/%s/%s' % (self.packageName, self.packageVersion))
-        self.descFileBasename = self.packageFullname + '.xml'
+        self.packageDir = Filename(host.hostDir, '%s/%s' % (self.packageName, self.packageVersion))
 
         # 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.displayName = None
+        self.uncompressedArchive = None
+        self.compressedArchive = None
+        self.extracts = []
+
+        # These are incremented during downloadPackage().
+        self.bytesDownloaded = 0
+        self.bytesUncompressed = 0
+        self.bytesUnpacked = 0
+        
+        # This is set true when the package file has been fully
+        # downloaded and unpackaged.
+        self.hasPackage = False
+
+    def getDownloadSize(self):
+        """ Returns the number of bytes we will need to download in
+        order to install this package. """
+        if self.hasPackage:
+            return 0
+        return self.compressedArchive.size
+
+    def getUncompressSize(self):
+        """ Returns the number of bytes we will need to uncompress in
+        order to install this package. """
+        if self.hasPackage:
+            return 0
+        return self.uncompressedArchive.size
+
+    def getUnpackSize(self):
+        """ Returns the number of bytes that we will need to unpack
+        when installing the package. """
+
+        if self.hasPackage:
+            return 0
+
+        size = 0
+        for file in self.extracts:
+            size += file.size
+        return size
+
+    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]
+        self.descFileBasename = basename
+
+    def checkStatus(self):
+        """ Checks the current status of the desc file and the package
+        contents on disk. """
+
+        if self.hasPackage:
+            return True
+
+        if not self.hasDescFile:
+            filename = Filename(self.packageDir, self.descFileBasename)
+            if self.descFile.quickVerify(self.packageDir, pathname = filename):
+                self.readDescFile()
+
+        if self.hasDescFile:
+            if self.__checkArchiveStatus():
+                # It's all good.
+                self.hasPackage = True
+
+        return self.hasPackage
+
+    def downloadDescFile(self, http):
+        """ Downloads the desc file for this particular package,
+        synchronously, and then reads it.  Returns true on success,
+        false on failure. """
+
+        assert self.descFile
+
+        if self.hasDescFile:
+            # We've already got one.
+            return True
+
+        url = URLSpec(self.descFileUrl)
+        print "Downloading %s" % (url)
+
+        rf = Ramfile()
+        channel = http.getDocument(url)
+        if not channel.downloadToRam(rf):
+            print "Unable to download %s" % (url)
+            return False
+
+        filename = Filename(self.packageDir, self.descFileBasename)
+        filename.makeDir()
+        f = open(filename.toOsSpecific(), 'wb')
+        f.write(rf.getData())
+        f.close()
+
+        try:
+            self.readDescFile()
+        except ValueError:
+            print "Failure reading %s" % (filename)
+            return False
+
+        return True
+
+    def readDescFile(self):
+        """ Reads the desc xml file for this particular package.
+        Presumably this has already been downloaded and installed. """
+
+        if self.hasDescFile:
+            # No need to read it again.
+            return
+
+        filename = Filename(self.packageDir, self.descFileBasename)
+
+        doc = TiXmlDocument(filename.toOsSpecific())
+        if not doc.LoadFile():
+            raise ValueError
+
+        xpackage = doc.FirstChildElement('package')
+        if not xpackage:
+            raise ValueError
+
+        # The name for display to an English-speaking user.
+        self.displayName = xpackage.Attribute('display_name')
+
+        # The uncompressed archive, which will be mounted directly,
+        # and also used for patching.
+        xuncompressedArchive = xpackage.FirstChildElement('uncompressed_archive')
+        if xuncompressedArchive:
+            self.uncompressedArchive = FileSpec()
+            self.uncompressedArchive.loadXml(xuncompressedArchive)
+
+        # The compressed archive, which is what is downloaded.
+        xcompressedArchive = xpackage.FirstChildElement('compressed_archive')
+        if xcompressedArchive:
+            self.compressedArchive = FileSpec()
+            self.compressedArchive.loadXml(xcompressedArchive)
+
+        # The list of files that should be extracted to disk.
+        xextract = xpackage.FirstChildElement('extract')
+        while xextract:
+            file = FileSpec()
+            file.loadXml(xextract)
+            self.extracts.append(file)
+            xextract = xextract.NextSiblingElement('extract')
+
+        self.hasDescFile = True
+
+        # Now that we've read the desc file, go ahead and use it to
+        # verify the download status.
+        if self.__checkArchiveStatus():
+            # It's all good.
+            self.hasPackage = True
+            return
+
+        # We need to download an update.
+        self.hasPackage = False
+
+    def __checkArchiveStatus(self):
+        """ Returns true if the archive and all extractable files are
+        already correct on disk, false otherwise. """
+        
+        allExtractsOk = True
+        if not self.uncompressedArchive.quickVerify(self.packageDir):
+            print "File is incorrect: %s" % (self.uncompressedArchive.filename)
+            allExtractsOk = False
+
+        if allExtractsOk:
+            for file in self.extracts:
+                if not file.quickVerify(self.packageDir):
+                    print "File is incorrect: %s" % (file.filename)
+                    allExtractsOk = False
+                    break
+
+        return allExtractsOk
+
+    def downloadPackage(self, http):
+        """ Downloads the package file, synchronously, then
+        uncompresses and unpacks it.  Returns true on success, false
+        on failure. """
+
+        assert self.hasDescFile
+
+        if self.hasPackage:
+            # We've already got one.
+            return True
+
+        if self.uncompressedArchive.quickVerify(self.packageDir):
+            return self.__unpackArchive()
+
+        if self.compressedArchive.quickVerify(self.packageDir):
+            return self.__uncompressArchive()
+
+        url = self.descFileUrl.rsplit('/', 1)[0]
+        url += '/' + self.compressedArchive.filename
+        url = DocumentSpec(url)
+        print "Downloading %s" % (url)
+
+        targetPathname = Filename(self.packageDir, self.compressedArchive.filename)
+        targetPathname.setBinary()
+        
+        channel = http.makeChannel(False)
+        channel.beginGetDocument(url)
+        channel.downloadToFile(targetPathname)
+        while channel.run():
+            self.bytesDownloaded = channel.getBytesDownloaded()
+            Thread.considerYield()
+        self.bytesDownloaded = channel.getBytesDownloaded()
+        if not channel.isValid():
+            print "Failed to download %s" % (url)
+            return False
+
+        if not self.compressedArchive.fullVerify(self.packageDir):
+            print "after downloading, %s still incorrect" % (
+                self.compressedArchive.filename)
+            return False
+        
+        return self.__uncompressArchive()
+
+    def __uncompressArchive(self):
+        """ Turns the compressed archive into the uncompressed
+        archive, then unpacks it.  Returns true on success, false on
+        failure. """
+
+        sourcePathname = Filename(self.packageDir, self.compressedArchive.filename)
+        targetPathname = Filename(self.packageDir, self.uncompressedArchive.filename)
+
+        print sourcePathname, targetPathname
+        decompressor = Decompressor()
+        decompressor.initiate(sourcePathname, targetPathname)
+        totalBytes = self.uncompressedArchive.size
+        result = decompressor.run()
+        while result == EUOk:
+            self.bytesUncompressed = int(totalBytes * decompressor.getProgress())
+            result = decompressor.run()
+            Thread.considerYield()
+
+        if result != EUSuccess:
+            return False
+            
+        self.bytesUncompressed = totalBytes
+
+        if not self.uncompressedArchive.quickVerify(self.packageDir):
+            print "after uncompressing, %s still incorrect" % (
+                self.uncompressedArchive.filename)
+            return False
+
+        return self.__unpackArchive()
+    
+    def __unpackArchive(self):
+        """ Unpacks any files in the archive that want to be unpacked
+        to disk. """
+
+        if not self.extracts:
+            # Nothing to extract.
+            self.hasPackage = True
+            return True
+
+        mfPathname = Filename(self.packageDir, self.uncompressedArchive.filename)
+        mf = Multifile()
+        if not mf.openRead(mfPathname):
+            print "Couldn't open %s" % (mfPathname)
+            return False
+        
+        allExtractsOk = True
+        self.bytesUnpacked = 0
+        for file in self.extracts:
+            i = mf.findSubfile(file.filename)
+            if i == -1:
+                print "Not in Multifile: %s" % (file.filename)
+                allExtractsOk = False
+                continue
+
+            targetPathname = Filename(self.packageDir, file.filename)
+            if not mf.extractSubfile(i, targetPathname):
+                print "Couldn't extract: %s" % (file.filename)
+                allExtractsOk = False
+                continue
+            
+            if not file.quickVerify(self.packageDir):
+                print "After extracting, still incorrect: %s" % (file.filename)
+                allExtractsOk = False
+                continue
+
+            # Make sure it's executable.
+            os.chmod(targetPathname.toOsSpecific(), 0755)
+
+            self.bytesUnpacked += file.size
+            Thread.considerYield()
+
+        if not allExtractsOk:
+            return False
+
+        self.hasPackage = True
+        return True
+
+    def installPackage(self, appRunner):
+        """ Mounts the package and sets up system paths so it becomes
+        available for use. """
+
+        assert self.hasPackage
+        
+        mfPathname = Filename(self.packageDir, self.uncompressedArchive.filename)
+        mf = Multifile()
+        if not mf.openRead(mfPathname):
+            print "Couldn't open %s" % (mfPathname)
+            return False
+
+        # We mount it under its actual location on disk.
+        root = self.packageDir.cStr()
+
+        vfs = VirtualFileSystem.getGlobalPtr()
+        vfs.mount(mf, root, vfs.MFReadOnly)
+
+        appRunner.loadMultifilePrcFiles(mf, root)
+
+        if root not in sys.path:
+            sys.path.append(root)
+        print "Installed %s %s" % (self.packageName, self.packageVersion)
+        
+        

+ 548 - 0
direct/src/p3d/PackageInstaller.py

@@ -0,0 +1,548 @@
+from direct.showbase.DirectObject import DirectObject
+from direct.stdpy.threading import Lock
+
+class PackageInstaller(DirectObject):
+
+    """ This class is used in a p3d runtime environment to manage the
+    asynchronous download and installation of packages.  If you just
+    want to install a package synchronously, see
+    appRunner.installPackage() for a simpler interface.
+
+    To use this class, you should subclass from it and override any of
+    the six callback methods: downloadStarted(), packageStarted(),
+    packageProgress(), downloadProgress(), packageFinished(),
+    downloadFinished().
+
+    Also see DWBPackageInstaller, which does exactly this, to add a
+    DirectWaitBar GUI.
+
+    Note that in the default mode, with a one-thread task chain, the
+    packages will all be downloaded in sequence, one after the other.
+    If you add more tasks to the task chain, some of the packages may
+    be downloaded in parallel, and the calls to packageStarted()
+    .. packageFinished() may therefore overlap.
+    """
+
+    globalLock = Lock()
+    nextUniqueId = 1
+
+    # This is a chain of state values progressing forward in time.
+    S_initial = 0    # addPackage() calls are being made
+    S_ready = 1      # donePackages() has been called
+    S_started = 2    # download has started
+    S_done = 3       # download is over
+    
+    class PendingPackage:
+        """ This class describes a package added to the installer for
+        download. """
+
+        # Weight factors for computing download progress.  This
+        # attempts to reflect the relative time-per-byte of each of
+        # these operations.
+        downloadFactor = 1
+        uncompressFactor = 0.02
+        unpackFactor = 0.01
+        
+        def __init__(self, packageName, version, host):
+            self.packageName = packageName
+            self.version = version
+            self.host = host
+
+            # Filled in by getDescFile().
+            self.package = None
+
+            self.done = False
+            self.success = False
+
+            self.calledPackageStarted = False
+            self.calledPackageFinished = False
+
+            # This is the amount of stuff we have to process to
+            # install this package, and the amount of stuff we have
+            # processed so far.  "Stuff" includes bytes downloaded,
+            # bytes uncompressed, and bytes extracted; and each of
+            # which is weighted differently into one grand total.  So,
+            # the total doesn't really represent bytes; it's a
+            # unitless number, which means something only as a ratio.
+            self.targetDownloadSize = 0
+
+        def getCurrentDownloadSize(self):
+            """ Returns the current amount of stuff we have processed
+            so far in the download. """
+            if self.done:
+                return self.targetDownloadSize
+
+            return (
+                self.package.bytesDownloaded * self.downloadFactor +
+                self.package.bytesUncompressed * self.uncompressFactor +
+                self.package.bytesUnpacked * self.unpackFactor)
+
+        def getProgress(self):
+            """ Returns the download progress of this package in the
+            range 0..1. """
+
+            if not self.targetDownloadSize:
+                return 1
+
+            return float(self.getCurrentDownloadSize()) / float(self.targetDownloadSize)
+
+        def getDescFile(self, http):
+            """ Synchronously downloads the desc files required for
+            the package. """
+            
+            if not self.host.downloadContentsFile(http):
+                return False
+
+            # All right, get the package info now.
+            self.package = self.host.getPackage(self.packageName, self.version)
+            if not self.package:
+                print "Package %s %s not known on %s" % (
+                    self.packageName, self.version, self.host.hostUrl)
+                return False
+
+            if not self.package.downloadDescFile(http):
+                return False
+
+            self.package.checkStatus()
+            self.targetDownloadSize = (
+                self.package.getDownloadSize() * self.downloadFactor +
+                self.package.getUncompressSize() * self.uncompressFactor +
+                self.package.getUnpackSize() * self.unpackFactor)
+            
+            return True
+
+    def __init__(self, appRunner, taskChain = 'install'):
+        self.globalLock.acquire()
+        try:
+            self.uniqueId = PackageInstaller.nextUniqueId
+            PackageInstaller.nextUniqueId += 1
+        finally:
+            self.globalLock.release()
+        
+        self.appRunner = appRunner
+        self.taskChain = taskChain
+        
+        # If the task chain hasn't yet been set up, create the
+        # default parameters now.
+        if not taskMgr.hasTaskChain(self.taskChain):
+            taskMgr.setupTaskChain(self.taskChain, numThreads = 1)
+
+        self.callbackLock = Lock()
+        self.calledDownloadStarted = False
+        self.calledDownloadFinished = False
+
+        # A list of all packages that have been added to the
+        # installer.
+        self.packageLock = Lock()
+        self.packages = []
+        self.state = self.S_initial
+
+        # A list of packages that are waiting for their desc files.
+        self.needsDescFile = []
+        self.descFileTask = None
+        
+        # A list of packages that are waiting to be downloaded and
+        # installed.
+        self.needsDownload = []
+        self.downloadTask = None
+
+        # A list of packages that have been successfully installed, or
+        # packages that have failed.
+        self.done = []
+        self.failed = []
+
+        # This task is spawned on the default task chain, to update
+        # the status during the download.
+        self.progressTask = None
+
+        # The totalDownloadSize is None, until all package desc files
+        # have been read.
+        self.totalDownloadSize = None
+        
+        self.accept('PackageInstaller-%s-allHaveDesc' % self.uniqueId,
+                    self.__allHaveDesc)
+        self.accept('PackageInstaller-%s-packageStarted' % self.uniqueId,
+                    self.__packageStarted)
+        self.accept('PackageInstaller-%s-packageDone' % self.uniqueId,
+                    self.__packageDone)
+
+    def destroy(self):
+        """ Interrupts all pending downloads.  No further callbacks
+        will be made. """
+        self.cleanup()
+
+    def cleanup(self):
+        """ Interrupts all pending downloads.  No further callbacks
+        will be made. """
+
+        self.packageLock.acquire()
+        try:
+            if self.descFileTask:
+                taskMgr.remove(self.descFileTask)
+                self.descFileTask = None
+            if self.downloadTask:
+                taskMgr.remove(self.downloadTask)
+                self.downloadTask = None
+        finally:
+            self.packageLock.release()
+
+        if self.progressTask:
+            taskMgr.remove(self.progressTask)
+            self.progressTask = None
+
+        self.ignoreAll()
+        
+    def addPackage(self, packageName, version = None, hostUrl = None):
+        """ Adds the named package to the list of packages to be
+        downloaded.  Call donePackages() to finish the list. """
+
+        if self.state != self.S_initial:
+            raise ValueError, 'addPackage called after donePackages'
+
+        host = self.appRunner.getHost(hostUrl)
+        pp = self.PendingPackage(packageName, version, host)
+
+        self.packageLock.acquire()
+        try:
+            self.packages.append(pp)
+            self.needsDescFile.append(pp)
+            if not self.descFileTask:
+                self.descFileTask = taskMgr.add(
+                    self.__getDescFileTask, 'getDescFile',
+                    taskChain = self.taskChain)
+        finally:
+            self.packageLock.release()
+
+    def donePackages(self):
+        """ After calling addPackage() for each package to be
+        installed, call donePackages() to mark the end of the list.
+        This is necessary to determine what the complete set of
+        packages is (and therefore how large the total download size
+        is).  Until this is called, no low-level callbacks will be
+        made as the packages are downloading. """
+
+        if self.state != self.S_initial:
+            # We've already been here.
+            return
+
+        working = True
+        
+        self.packageLock.acquire()
+        try:
+            if self.state != self.S_initial:
+                return
+            self.state = self.S_ready
+            if not self.needsDescFile:
+                # All package desc files are already available; so begin.
+                working = self.__prepareToStart()
+        finally:
+            self.packageLock.release()
+
+        if not working:
+            self.downloadFinished(True)
+
+    def downloadStarted(self):
+        """ This callback is made at some point after donePackages()
+        is called; at the time of this callback, the total download
+        size is known, and we can sensibly report progress through the
+        whole. """
+        pass
+
+    def packageStarted(self, package):
+        """ This callback is made for each package between
+        downloadStarted() and downloadFinished() to indicate the start
+        of a new package. """
+        pass
+
+    def packageProgress(self, package, progress):
+        """ This callback is made repeatedly between packageStarted()
+        and packageFinished() to update the current progress on the
+        indicated package only.  The progress value ranges from 0
+        (beginning) to 1 (complete). """
+        pass
+        
+    def downloadProgress(self, overallProgress):
+        """ This callback is made repeatedly between downloadStarted()
+        and downloadFinished() to update the current progress through
+        all packages.  The progress value ranges from 0 (beginning) to
+        1 (complete). """
+        pass
+
+    def packageFinished(self, package, success):
+        """ This callback is made for each package between
+        downloadStarted() and downloadFinished() to indicate that a
+        package has finished downloading.  If success is true, there
+        were no problems and the package is now installed.
+
+        If this package did not require downloading (because it was
+        already downloaded), this callback will be made immediately,
+        *without* a corresponding call to packageStarted(), and may
+        even be made before downloadStarted(). """
+        pass
+
+    def downloadFinished(self, success):
+        """ This callback is made when all of the packages have been
+        downloaded and installed (or there has been some failure).  If
+        all packages where successfully installed, success is True.
+
+        If there were no packages that required downloading, this
+        callback will be made immediately, *without* a corresponding
+        call to downloadStarted(). """
+        pass
+
+    def __prepareToStart(self):
+        """ This is called internally when transitioning from S_ready
+        to S_started.  It sets up whatever initial values are
+        needed.  Assumes self.packageLock is held.  Returns False if
+        there were no packages to download, and the state was
+        therefore transitioned immediately to S_done. """
+
+        if not self.needsDownload:
+            self.state = self.S_done
+            return False
+
+        self.state = self.S_started
+
+        assert not self.downloadTask
+        self.downloadTask = taskMgr.add(
+            self.__downloadPackageTask, 'downloadPackage',
+            taskChain = self.taskChain)
+
+        assert not self.progressTask
+        self.progressTask = taskMgr.add(
+            self.__progressTask, 'packageProgress')
+
+        return True
+
+    def __allHaveDesc(self):
+        """ This method is called internally when all of the pending
+        packages have their desc info. """
+        working = True
+        
+        self.packageLock.acquire()
+        try:
+            if self.state == self.S_ready:
+                # We've already called donePackages(), so move on now.
+                working = self.__prepareToStart()
+        finally:
+            self.packageLock.release()
+
+        if not working:
+            self.__callDownloadFinished(True)
+
+    def __packageStarted(self, pp):
+        """ This method is called when a single package is beginning
+        to download. """
+        print "Downloading %s" % (pp.packageName)
+        self.__callDownloadStarted()
+        self.__callPackageStarted(pp)
+
+    def __packageDone(self, pp):
+        """ This method is called when a single package has been
+        downloaded and installed, or has failed. """
+        print "Downloaded %s: %s" % (pp.packageName, pp.success)
+        self.__callPackageFinished(pp, pp.success)
+
+        if not pp.calledPackageStarted:
+            # Trivially done; this one was done before it got started.
+            return
+
+        assert self.state == self.S_started
+        # See if there are more packages to go.
+        success = True
+        allDone = True
+        self.packageLock.acquire()
+        try:
+            assert self.state == self.S_started
+            for pp in self.packages:
+                if pp.done:
+                    success = success and pp.success
+                else:
+                    allDone = False
+        finally:
+            self.packageLock.release()
+
+        if allDone:
+            self.__callDownloadFinished(success)
+
+    def __callPackageStarted(self, pp):
+        """ Calls the packageStarted() callback for a particular
+        package if it has not already been called, being careful to
+        avoid race conditions. """
+
+        self.callbackLock.acquire()
+        try:
+            if not pp.calledPackageStarted:
+                self.packageStarted(pp.package)
+                self.packageProgress(pp.package, 0)
+                pp.calledPackageStarted = True
+        finally:
+            self.callbackLock.release()
+
+    def __callPackageFinished(self, pp, success):
+        """ Calls the packageFinished() callback for a paricular
+        package if it has not already been called, being careful to
+        avoid race conditions. """
+
+        self.callbackLock.acquire()
+        try:
+            if not pp.calledPackageFinished:
+                if success:
+                    self.packageProgress(pp.package, 1)
+                self.packageFinished(pp.package, success)
+                pp.calledPackageFinished = True
+        finally:
+            self.callbackLock.release()
+
+    def __callDownloadStarted(self):
+        """ Calls the downloadStarted() callback if it has not already
+        been called, being careful to avoid race conditions. """
+
+        self.callbackLock.acquire()
+        try:
+            if not self.calledDownloadStarted:
+                self.downloadStarted()
+                self.downloadProgress(0)
+                self.calledDownloadStarted = True
+        finally:
+            self.callbackLock.release()
+
+    def __callDownloadFinished(self, success):
+        """ Calls the downloadFinished() callback if it has not
+        already been called, being careful to avoid race
+        conditions. """
+
+        self.callbackLock.acquire()
+        try:
+            if not self.calledDownloadFinished:
+                if success:
+                    self.downloadProgress(1)
+                self.downloadFinished(success)
+                self.calledDownloadFinished = True
+        finally:
+            self.callbackLock.release()
+
+    def __getDescFileTask(self, task):
+
+        """ This task runs on the aysynchronous task chain; each pass,
+        it extracts one package from self.needsDescFile and downloads
+        its desc file.  On success, it adds the package to
+        self.needsDownload. """
+        
+        self.packageLock.acquire()
+        try:
+            # If we've finished all of the packages that need desc
+            # files, stop the task.
+            if not self.needsDescFile:
+                self.descFileTask = None
+                messenger.send('PackageInstaller-%s-allHaveDesc' % self.uniqueId,
+                               taskChain = 'default')
+                return task.done
+            pp = self.needsDescFile[0]
+            del self.needsDescFile[0]
+        finally:
+            self.packageLock.release()
+
+        # Now serve this one package.
+        if not pp.getDescFile(self.appRunner.http):
+            self.__donePackage(pp, False)
+            return task.cont
+
+        if pp.package.hasPackage:
+            # This package is already downloaded.
+            self.__donePackage(pp, True)
+            return task.cont
+
+        # This package is now ready to be downloaded.
+        self.packageLock.acquire()
+        try:
+            self.needsDownload.append(pp)
+        finally:
+            self.packageLock.release()
+
+        return task.cont
+        
+    def __downloadPackageTask(self, task):
+
+        """ 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:
+            self.packageLock.release()
+
+        # Now serve this one package.
+        messenger.send('PackageInstaller-%s-packageStarted' % self.uniqueId,
+                       [pp], taskChain = 'default')
+        
+        if not pp.package.downloadPackage(self.appRunner.http):
+            self.__donePackage(pp, False)
+            return task.cont
+
+        pp.package.installPackage(self.appRunner)
+
+        # Successfully downloaded and installed.
+        self.__donePackage(pp, True)
+        
+        return task.cont
+        
+    def __donePackage(self, pp, success):
+        """ Marks the indicated package as done, either successfully
+        or otherwise. """
+        assert not pp.done
+
+        self.packageLock.acquire()
+        try:
+            pp.done = True
+            pp.success = success
+            if success:
+                self.done.append(pp)
+            else:
+                self.failed.append(pp)
+        finally:
+            self.packageLock.release()
+
+        messenger.send('PackageInstaller-%s-packageDone' % self.uniqueId,
+                       [pp], taskChain = 'default')
+
+    def __progressTask(self, task):
+        self.callbackLock.acquire()
+        try:
+            if not self.calledDownloadStarted:
+                # We haven't yet officially started the download.
+                return task.cont
+
+            if self.calledDownloadFinished:
+                # We've officially ended the download.
+                self.progressTask = None
+                return task.done
+
+            targetDownloadSize = 0
+            currentDownloadSize = 0
+            for pp in self.packages:
+                targetDownloadSize += pp.targetDownloadSize
+                currentDownloadSize += pp.getCurrentDownloadSize()
+                if pp.calledPackageStarted and not pp.calledPackageFinished:
+                    self.packageProgress(pp.package, pp.getProgress())
+
+            if not targetDownloadSize:
+                progress = 1
+            else:
+                progress = float(currentDownloadSize) / float(targetDownloadSize)
+            self.downloadProgress(progress)
+            
+        finally:
+            self.callbackLock.release()
+
+        return task.cont
+    

+ 1 - 1
direct/src/p3d/packp3d.py

@@ -51,7 +51,7 @@ from direct.p3d import Packager
 from pandac.PandaModules import *
 
 # Temp hack for debugging.
-#from direct.p3d.AppRunner import dummyAppRunner; dummyAppRunner(fullDiskAccess = 1)
+#from direct.p3d.AppRunner import dummyAppRunner; dummyAppRunner()
 
 class ArgumentError(StandardError):
     pass

+ 0 - 2
direct/src/p3d/panda3d.pdef

@@ -147,7 +147,6 @@ class packp3d(p3d):
     # the targeted runtime.
 
     config(display_name = "Panda3D Application Packer",
-           full_disk_access = True,
            hidden = True)
     require('panda3d', 'egg')
 
@@ -160,7 +159,6 @@ class ppackage(p3d):
     # more packages or p3d applications.
 
     config(display_name = "Panda3D General Package Utility",
-           full_disk_access = True,
            hidden = True)
     require('panda3d', 'egg')
 

+ 4 - 2
direct/src/plugin/load_plugin.cxx

@@ -126,7 +126,8 @@ bool
 load_plugin(const string &p3d_plugin_filename, 
             const string &contents_filename, const string &download_url, 
             bool verify_contents, const string &platform,
-            const string &log_directory, const string &log_basename) {
+            const string &log_directory, const string &log_basename,
+            bool keep_cwd) {
   string filename = p3d_plugin_filename;
   if (filename.empty()) {
     // Look for the plugin along the path.
@@ -297,7 +298,8 @@ load_plugin(const string &p3d_plugin_filename,
 
   if (!P3D_initialize(P3D_API_VERSION, contents_filename.c_str(),
                       download_url.c_str(), verify_contents, platform.c_str(),
-                      log_directory.c_str(), log_basename.c_str())) {
+                      log_directory.c_str(), log_basename.c_str(),
+                      keep_cwd)) {
     // Oops, failure to initialize.
     cerr << "Failed to initialize plugin (wrong API version?)\n";
     unload_plugin();

+ 2 - 1
direct/src/plugin/load_plugin.h

@@ -62,7 +62,8 @@ bool
 load_plugin(const string &p3d_plugin_filename, 
             const string &contents_filename, const string &download_url,
             bool verify_contents, const string &platform,
-            const string &log_directory, const string &log_basename);
+            const string &log_directory, const string &log_basename,
+            bool keep_cwd);
 void unload_plugin();
 bool is_plugin_loaded();
 

+ 0 - 6
direct/src/plugin/p3dInstance.cxx

@@ -71,7 +71,6 @@ P3DInstance(P3D_request_ready_func *func,
 
   P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
   _instance_id = inst_mgr->get_unique_id();
-  _full_disk_access = false;
   _hidden = false;
   _session = NULL;
   _panda3d = NULL;
@@ -882,11 +881,6 @@ scan_app_desc_file(TiXmlDocument *doc) {
     _log_basename = log_basename;
   }
 
-  int full_disk_access = 0;
-  if (xpackage->QueryIntAttribute("full_disk_access", &full_disk_access) == TIXML_SUCCESS) {
-    _full_disk_access = (full_disk_access != 0);
-  }
-
   int hidden = 0;
   if (xpackage->QueryIntAttribute("hidden", &hidden) == TIXML_SUCCESS) {
     _hidden = (hidden != 0);

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

@@ -160,7 +160,6 @@ private:
   string _session_key;
   string _python_version;
   string _log_basename;
-  bool _full_disk_access;
   bool _hidden;
 
   P3DSession *_session;

+ 14 - 0
direct/src/plugin/p3dInstanceManager.I

@@ -89,6 +89,20 @@ get_log_directory() const {
   return _log_directory;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: P3DInstanceManager::get_keep_cwd
+//       Access: Public
+//  Description: Returns the value of the keep_cwd flag passed to the
+//               constructor.  This is true if the original working
+//               directory was valuable and meaningful, and should be
+//               preserved; or false if it is meaningless and should
+//               be changed.
+////////////////////////////////////////////////////////////////////
+inline bool P3DInstanceManager::
+get_keep_cwd() const {
+  return _keep_cwd;
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DInstanceManager::get_num_instances
 //       Access: Public

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

@@ -51,6 +51,7 @@ P3DInstanceManager() {
   _is_initialized = false;
   _next_temp_filename_counter = 1;
   _unique_id = 0;
+  _keep_cwd = false;
 
   _notify_thread_continue = false;
   _started_notify_thread = false;
@@ -155,8 +156,8 @@ bool P3DInstanceManager::
 initialize(const string &contents_filename, const string &download_url,
            bool verify_contents,
            const string &platform, const string &log_directory,
-           const string &log_basename) {
-
+           const string &log_basename, bool keep_cwd) {
+  _keep_cwd = keep_cwd;
   _root_dir = find_root_dir();
   _verify_contents = verify_contents;
   _platform = platform;

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

@@ -48,7 +48,8 @@ public:
                   bool verify_contents,
                   const string &platform,
                   const string &log_directory,
-                  const string &log_basename);
+                  const string &log_basename,
+                  bool keep_cwd);
 
   inline bool is_initialized() const;
   inline bool get_verify_contents() const;
@@ -57,6 +58,7 @@ public:
   inline const string &get_root_dir() const;
   inline const string &get_platform() const;
   inline const string &get_log_directory() const;
+  inline bool get_keep_cwd() const;
 
   P3DInstance *
   create_instance(P3D_request_ready_func *func, 
@@ -108,6 +110,7 @@ private:
   string _log_basename;
   string _log_pathname;
   string _temp_directory;
+  bool _keep_cwd;
 
   P3D_object *_undefined_object;
   P3D_object *_none_object;

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

@@ -345,8 +345,8 @@ desc_file_download_finished(bool success) {
 void P3DPackage::
 got_desc_file(TiXmlDocument *doc, bool freshly_downloaded) {
   TiXmlElement *xpackage = doc->FirstChildElement("package");
-  TiXmlElement *uncompressed_archive = NULL;
-  TiXmlElement *compressed_archive = NULL;
+  TiXmlElement *xuncompressed_archive = NULL;
+  TiXmlElement *xcompressed_archive = NULL;
   
   if (xpackage != NULL) {
     const char *display_name_cstr = xpackage->Attribute("display_name");
@@ -354,11 +354,11 @@ got_desc_file(TiXmlDocument *doc, bool freshly_downloaded) {
       _package_display_name = display_name_cstr;
     }
 
-    uncompressed_archive = xpackage->FirstChildElement("uncompressed_archive");
-    compressed_archive = xpackage->FirstChildElement("compressed_archive");
+    xuncompressed_archive = xpackage->FirstChildElement("uncompressed_archive");
+    xcompressed_archive = xpackage->FirstChildElement("compressed_archive");
   }
 
-  if (uncompressed_archive == NULL || compressed_archive == NULL) {
+  if (xuncompressed_archive == NULL || xcompressed_archive == NULL) {
     // The desc file didn't include the archive file itself, weird.
     if (!freshly_downloaded) {
       download_desc_file();
@@ -368,8 +368,8 @@ got_desc_file(TiXmlDocument *doc, bool freshly_downloaded) {
     return;
   }
 
-  _uncompressed_archive.load_xml(uncompressed_archive);
-  _compressed_archive.load_xml(compressed_archive);
+  _uncompressed_archive.load_xml(xuncompressed_archive);
+  _compressed_archive.load_xml(xcompressed_archive);
 
   // Now get all the extractable components.
   _extracts.clear();
@@ -472,8 +472,6 @@ download_compressed_archive(bool allow_partial) {
     url = url.substr(0, slash + 1);
   }
   url += _compressed_archive.get_filename();
-  cerr << "_desc_file_url = " << _desc_file_url << ", url = " << url 
-       << "\n";
 
   string target_pathname = _package_dir + "/" + _compressed_archive.get_filename();
 

+ 4 - 6
direct/src/plugin/p3dSession.cxx

@@ -654,13 +654,11 @@ start_p3dpython(P3DInstance *inst) {
 
   _python_root_dir = inst->_panda3d->get_package_dir();
 
-  // We'll be changing the directory to the standard start directory
-  // only if we don't have full disk access set for the instance.  If
-  // we do have this setting, we'll keep the current directory
-  // instead.
-  bool change_dir = !inst->_full_disk_access;
+  // Change the current directory to the standard start directory, but
+  // only if the runtime environment told us the original current
+  // directory isn't meaningful.
   string start_dir;
-  if (change_dir) {
+  if (!inst_mgr->get_keep_cwd()) {
     start_dir = _start_dir;
     mkdir_complete(start_dir, nout);
   }

+ 4 - 2
direct/src/plugin/p3d_plugin.cxx

@@ -36,7 +36,8 @@ bool
 P3D_initialize(int api_version, const char *contents_filename,
                const char *download_url, bool verify_contents,
                const char *platform,
-               const char *log_directory, const char *log_basename) {
+               const char *log_directory, const char *log_basename,
+               bool keep_cwd) {
   if (api_version != P3D_API_VERSION) {
     // Can't accept an incompatible version.
     return false;
@@ -71,7 +72,8 @@ P3D_initialize(int api_version, const char *contents_filename,
   P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
   bool result = inst_mgr->initialize(contents_filename, download_url,
                                      verify_contents, platform,
-                                     log_directory, log_basename);
+                                     log_directory, log_basename,
+                                     keep_cwd);
   RELEASE_LOCK(_api_lock);
   return result;
 }

+ 8 - 1
direct/src/plugin/p3d_plugin.h

@@ -122,6 +122,12 @@ extern "C" {
    core API.  Note that the individual instances also have their own
    log_basename values.
 
+   Finally, keep_cwd should be set true if the current working
+   directory is meaningful and valuable to the user (for instance,
+   when this is launched via a command-line tool), or false if it
+   means nothing and can safely be reset.  Normally, a browser plugin
+   should set this false.
+
    This function returns true if the core API is valid and uses a
    compatible API, false otherwise.  If it returns false, the host
    should not call any more functions in this API, and should
@@ -130,7 +136,8 @@ typedef bool
 P3D_initialize_func(int api_version, const char *contents_filename,
                     const char *download_url, bool verify_contents,
                     const char *platform,
-                    const char *log_directory, const char *log_basename);
+                    const char *log_directory, const char *log_basename,
+                    bool keep_cwd);
 
 /* This function should be called to unload the core API.  It will
    release all internally-allocated memory and return the core API to

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

@@ -951,7 +951,7 @@ do_load_plugin() {
 #endif  // P3D_PLUGIN_P3D_PLUGIN
 
   nout << "Attempting to load core API from " << pathname << "\n";
-  if (!load_plugin(pathname, "", "", true, "", "", "")) {
+  if (!load_plugin(pathname, "", "", true, "", "", "", false)) {
     nout << "Unable to launch core API in " << pathname << "\n";
     return;
   }

+ 1 - 1
direct/src/plugin_standalone/panda3d.cxx

@@ -470,7 +470,7 @@ get_core_api(const Filename &contents_filename, const string &download_url,
 
   if (!load_plugin(pathname, contents_filename.to_os_specific(),
                    download_url, verify_contents, this_platform, _log_dirname,
-                   _log_basename)) {
+                   _log_basename, true)) {
     cerr << "Unable to launch core API in " << pathname << "\n" << flush;
     return false;
   }

+ 36 - 7
direct/src/showbase/Messenger.py

@@ -312,19 +312,48 @@ class Messenger:
             if taskChain:
                 # Queue the event onto the indicated task chain.
                 from direct.task.TaskManagerGlobal import taskMgr
-                taskMgr.add(self.__lockAndDispatch, name = 'Messenger-%s-%s' % (event, taskChain), extraArgs = [acceptorDict, event, sentArgs, foundWatch], taskChain = taskChain)
+                queue = self._eventQueuesByTaskChain.setdefault(taskChain, [])
+                queue.append((acceptorDict, event, sentArgs, foundWatch))
+                if len(queue) == 1:
+                    # If this is the first (only) item on the queue,
+                    # spawn the task to empty it.
+                    taskMgr.add(self.__taskChainDispatch, name = 'Messenger-%s' % (taskChain),
+                                extraArgs = [taskChain], taskChain = taskChain,
+                                appendTask = True)
             else:
                 # Handle the event immediately.
                 self.__dispatch(acceptorDict, event, sentArgs, foundWatch)
         finally:
             self.lock.release()
 
-    def __lockAndDispatch(self, acceptorDict, event, sentArgs, foundWatch):
-        self.lock.acquire()
-        try:
-            self.__dispatch(acceptorDict, event, sentArgs, foundWatch)
-        finally:
-            self.lock.release()
+    def __taskChainDispatch(self, taskChain, task):
+        """ This task is spawned each time an event is sent across
+        task chains.  Its job is to empty the task events on the queue
+        for this particular task chain.  This guarantees that events
+        are still delivered in the same order they were sent. """
+
+        while True:
+            eventTuple = None
+            self.lock.acquire()
+            try:
+                queue = self._eventQueuesByTaskChain.get(taskChain, None)
+                if queue:
+                    eventTuple = queue[0]
+                    del queue[0]
+                if not queue:
+                    # The queue is empty, we're done.
+                    if queue is not None:
+                        del self._eventQueuesByTaskChain[taskChain]
+
+                if not eventTuple:
+                    # No event; we're done.
+                    return task.done
+                
+                self.__dispatch(*eventTuple)
+            finally:
+                self.lock.release()
+
+        return task.done
 
     def __dispatch(self, acceptorDict, event, sentArgs, foundWatch):
         for id in acceptorDict.keys():

+ 1 - 1
direct/src/showbase/ShowBase.py

@@ -2483,7 +2483,7 @@ class ShowBase(DirectObject.DirectObject):
         # has to be responsible for running the main loop, so we can't
         # allow the application to do it.  This is a minor hack, but
         # should work for 99% of the cases.
-        if self.appRunner is None:
+        if self.appRunner is None or self.appRunner.dummy:
             self.taskMgr.run()
 
 

+ 10 - 0
direct/src/task/TaskNew.py

@@ -161,6 +161,16 @@ class TaskManager:
             # Next time around invoke the default handler
             signal.signal(signal.SIGINT, self.invokeDefaultHandler)
 
+    def hasTaskChain(self, chainName):
+        """ Returns true if a task chain with the indicated name has
+        already been defined, or false otherwise.  Note that
+        setupTaskChain() will implicitly define a task chain if it has
+        not already been defined, or modify an existing one if it has,
+        so in most cases there is no need to check this method
+        first. """
+
+        return (self.mgr.findTaskChain(chainName) != None)
+
     def setupTaskChain(self, chainName, numThreads = None, tickClock = None,
                        threadPriority = None, frameBudget = None,
                        timeslicePriority = None):