Browse Source

PackageInstaller, runtime package installation

David Rose 16 years ago
parent
commit
81363c4ba9

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

@@ -13,10 +13,9 @@ d = DirectWaitBar(borderWidth=(0, 0))
 """
 """
 
 
 class DirectWaitBar(DirectFrame):
 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):
     def __init__(self, parent = None, **kw):
         # Inherits from DirectFrame
         # Inherits from DirectFrame
         # A Direct Frame can have:
         # A Direct Frame can have:

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

@@ -36,6 +36,16 @@ class ScriptAttributes:
     pass
     pass
 
 
 class AppRunner(DirectObject):
 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):
     def __init__(self):
         DirectObject.__init__(self)
         DirectObject.__init__(self)
 
 
@@ -45,6 +55,9 @@ class AppRunner(DirectObject):
         # child.
         # child.
         sys.stdout = sys.stderr
         sys.stdout = sys.stderr
 
 
+        # This is set true by dummyAppRunner(), below.
+        self.dummy = False
+
         self.sessionId = 0
         self.sessionId = 0
         self.packedAppEnvironmentInitialized = False
         self.packedAppEnvironmentInitialized = False
         self.gotWindow = False
         self.gotWindow = False
@@ -54,8 +67,6 @@ class AppRunner(DirectObject):
         self.windowPrc = None
         self.windowPrc = None
         self.http = HTTPClient.getGlobalPtr()
         self.http = HTTPClient.getGlobalPtr()
 
 
-        self.fullDiskAccess = False
-
         self.Undefined = Undefined
         self.Undefined = Undefined
         self.ConcreteStruct = ConcreteStruct
         self.ConcreteStruct = ConcreteStruct
 
 
@@ -72,10 +83,18 @@ class AppRunner(DirectObject):
         # A list of the Panda3D packages that have been loaded.
         # A list of the Panda3D packages that have been loaded.
         self.installedPackages = []
         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
         # A dictionary of HostInfo objects for the various download
         # hosts we have imported packages from.
         # hosts we have imported packages from.
         self.hosts = {}
         self.hosts = {}
 
 
+        # Managing packages for runtime download.
+        self.downloadingPackages = []
+        self.downloadTask = None
+
         # The mount point for the multifile.  For now, this is always
         # The mount point for the multifile.  For now, this is always
         # the same, but when we move to multiple-instance sessions, it
         # the same, but when we move to multiple-instance sessions, it
         # may have to be different for each instance.
         # may have to be different for each instance.
@@ -116,6 +135,36 @@ class AppRunner(DirectObject):
         # call back to the main thread.
         # call back to the main thread.
         self.accept('AppRunner_startIfReady', self.__startIfReady)
         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):
     def getHost(self, hostUrl):
         """ Returns a new HostInfo object corresponding to the
         """ Returns a new HostInfo object corresponding to the
         indicated host URL.  If we have already seen this URL
         indicated host URL.  If we have already seen this URL
@@ -163,7 +212,7 @@ class AppRunner(DirectObject):
             return False
             return False
 
 
         return True
         return True
-            
+
     def stop(self):
     def stop(self):
         """ This method can be called by JavaScript to stop the
         """ This method can be called by JavaScript to stop the
         application. """
         application. """
@@ -191,40 +240,6 @@ class AppRunner(DirectObject):
 
 
         vfs = VirtualFileSystem.getGlobalPtr()
         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.
         # Now set up Python to import this stuff.
         VFSImporter.register()
         VFSImporter.register()
         sys.path = [ self.multifileRoot ] + sys.path
         sys.path = [ self.multifileRoot ] + sys.path
@@ -240,11 +255,6 @@ class AppRunner(DirectObject):
         os.listdir = file.listdir
         os.listdir = file.listdir
         os.walk = file.walk
         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):
     def __startIfReady(self):
         """ Called internally to start the application. """
         """ Called internally to start the application. """
         if self.started:
         if self.started:
@@ -320,7 +330,12 @@ class AppRunner(DirectObject):
         application. """
         application. """
 
 
         host = self.getHost(hostUrl)
         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:
         if not platform:
             platform = None
             platform = None
@@ -328,6 +343,14 @@ class AppRunner(DirectObject):
         assert package
         assert package
         self.installedPackages.append(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 = [],
     def setP3DFilename(self, p3dFilename, tokens = [], argv = [],
                        instanceId = None):
                        instanceId = None):
         """ Called by the browser to specify the p3d file that
         """ Called by the browser to specify the p3d file that
@@ -375,36 +398,36 @@ class AppRunner(DirectObject):
         if self.p3dInfo:
         if self.p3dInfo:
             self.p3dPackage = self.p3dInfo.FirstChildElement('package')
             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()
         self.initPackedAppEnvironment()
 
 
         # Mount the Multifile under /mf, by convention.
         # Mount the Multifile under /mf, by convention.
         vfs.mount(mf, self.multifileRoot, vfs.MFReadOnly)
         vfs.mount(mf, self.multifileRoot, vfs.MFReadOnly)
         VFSImporter.freeze_new_modules(mf, self.multifileRoot)
         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
         self.gotP3DFilename = True
 
 
         # Send this call to the main thread; don't call it directly.
         # Send this call to the main thread; don't call it directly.
         messenger.send('AppRunner_startIfReady', taskChain = 'default')
         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):
     def __clearWindowPrc(self):
         """ Clears the windowPrc file that was created in a previous
         """ Clears the windowPrc file that was created in a previous
         call to setupWindow(), if any. """
         call to setupWindow(), if any. """
@@ -563,7 +586,7 @@ class AppRunner(DirectObject):
 
 
         self.sendRequest('drop_p3dobj', objectId)
         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
     """ This function creates a dummy global AppRunner object, which
     is useful for testing running in a packaged environment without
     is useful for testing running in a packaged environment without
     actually bothering to package up the application.  Call this at
     actually bothering to package up the application.  Call this at
@@ -580,6 +603,7 @@ def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
         return
         return
 
 
     appRunner = AppRunner()
     appRunner = AppRunner()
+    appRunner.dummy = True
     AppRunnerGlobal.appRunner = appRunner
     AppRunnerGlobal.appRunner = appRunner
 
 
     platform = PandaSystem.getPlatform()
     platform = PandaSystem.getPlatform()
@@ -604,7 +628,6 @@ def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
 
 
     appRunner.p3dInfo = None
     appRunner.p3dInfo = None
     appRunner.p3dPackage = None
     appRunner.p3dPackage = None
-    appRunner.fullDiskAccess = fullDiskAccess
 
 
     # Mount the current directory under the multifileRoot, as if it
     # Mount the current directory under the multifileRoot, as if it
     # were coming from a multifile.
     # were coming from a multifile.
@@ -619,3 +642,5 @@ def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
     os.listdir = file.listdir
     os.listdir = file.listdir
     os.walk = file.walk
     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. """
         redownloaded. """
 
 
         if not pathname:
         if not pathname:
-            pathname = Filename(packageDir, pathname)
+            pathname = Filename(packageDir, self.filename)
         try:
         try:
             st = os.stat(pathname.toOsSpecific())
             st = os.stat(pathname.toOsSpecific())
         except OSError:
         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.PackageInfo import PackageInfo
 from direct.p3d.FileSpec import FileSpec
 from direct.p3d.FileSpec import FileSpec
 
 
@@ -31,6 +31,37 @@ class HostInfo:
         self.__determineHostDir(appRunner)
         self.__determineHostDir(appRunner)
         self.importsDir = Filename(self.hostDir, 'imports')
         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):
     def readContentsFile(self):
         """ Reads the contents.xml file for this particular host.
         """ Reads the contents.xml file for this particular host.
         Presumably this has already been downloaded and installed. """
         Presumably this has already been downloaded and installed. """
@@ -43,7 +74,7 @@ class HostInfo:
 
 
         doc = TiXmlDocument(filename.toOsSpecific())
         doc = TiXmlDocument(filename.toOsSpecific())
         if not doc.LoadFile():
         if not doc.LoadFile():
-            raise IOError
+            raise ValueError
 
 
         xcontents = doc.FirstChildElement('contents')
         xcontents = doc.FirstChildElement('contents')
         if not xcontents:
         if not xcontents:
@@ -60,6 +91,7 @@ class HostInfo:
             package = self.__makePackage(name, platform, version)
             package = self.__makePackage(name, platform, version)
             package.descFile = FileSpec()
             package.descFile = FileSpec()
             package.descFile.loadXml(xpackage)
             package.descFile.loadXml(xpackage)
+            package.setupFilenames()
 
 
             xpackage = xpackage.NextSiblingElement('package')
             xpackage = xpackage.NextSiblingElement('package')
 
 
@@ -100,6 +132,7 @@ class HostInfo:
         version and the indicated platform or the current runtime
         version and the indicated platform or the current runtime
         platform, if one is provided by this host, or None if not. """
         platform, if one is provided by this host, or None if not. """
 
 
+        assert self.hasContentsFile
         platforms = self.packages.get((name, version), {})
         platforms = self.packages.get((name, version), {})
 
 
         if platform is not None:
         if platform is not None:
@@ -115,7 +148,7 @@ class HostInfo:
         # If not found, look for one matching no particular platform.
         # If not found, look for one matching no particular platform.
         if not package:
         if not package:
             package = platforms.get(None, None)
             package = platforms.get(None, None)
-            
+
         return package
         return package
 
 
     def __determineHostDir(self, appRunner):
     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:
 class PackageInfo:
 
 
@@ -12,11 +15,333 @@ class PackageInfo:
         self.packageVersion = packageVersion
         self.packageVersion = packageVersion
         self.platform = platform
         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
         # These will be filled in by HostInfo when the package is read
         # from contents.xml.
         # from contents.xml.
+        self.descFileUrl = None
         self.descFile = None
         self.descFile = None
         self.importDescFile = 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 *
 from pandac.PandaModules import *
 
 
 # Temp hack for debugging.
 # Temp hack for debugging.
-#from direct.p3d.AppRunner import dummyAppRunner; dummyAppRunner(fullDiskAccess = 1)
+#from direct.p3d.AppRunner import dummyAppRunner; dummyAppRunner()
 
 
 class ArgumentError(StandardError):
 class ArgumentError(StandardError):
     pass
     pass

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

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

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

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

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

@@ -62,7 +62,8 @@ bool
 load_plugin(const string &p3d_plugin_filename, 
 load_plugin(const string &p3d_plugin_filename, 
             const string &contents_filename, const string &download_url,
             const string &contents_filename, const string &download_url,
             bool verify_contents, const string &platform,
             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();
 void unload_plugin();
 bool is_plugin_loaded();
 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();
   P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
   _instance_id = inst_mgr->get_unique_id();
   _instance_id = inst_mgr->get_unique_id();
-  _full_disk_access = false;
   _hidden = false;
   _hidden = false;
   _session = NULL;
   _session = NULL;
   _panda3d = NULL;
   _panda3d = NULL;
@@ -882,11 +881,6 @@ scan_app_desc_file(TiXmlDocument *doc) {
     _log_basename = log_basename;
     _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;
   int hidden = 0;
   if (xpackage->QueryIntAttribute("hidden", &hidden) == TIXML_SUCCESS) {
   if (xpackage->QueryIntAttribute("hidden", &hidden) == TIXML_SUCCESS) {
     _hidden = (hidden != 0);
     _hidden = (hidden != 0);

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

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

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

@@ -89,6 +89,20 @@ get_log_directory() const {
   return _log_directory;
   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
 //     Function: P3DInstanceManager::get_num_instances
 //       Access: Public
 //       Access: Public

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

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

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

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

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

@@ -345,8 +345,8 @@ desc_file_download_finished(bool success) {
 void P3DPackage::
 void P3DPackage::
 got_desc_file(TiXmlDocument *doc, bool freshly_downloaded) {
 got_desc_file(TiXmlDocument *doc, bool freshly_downloaded) {
   TiXmlElement *xpackage = doc->FirstChildElement("package");
   TiXmlElement *xpackage = doc->FirstChildElement("package");
-  TiXmlElement *uncompressed_archive = NULL;
-  TiXmlElement *compressed_archive = NULL;
+  TiXmlElement *xuncompressed_archive = NULL;
+  TiXmlElement *xcompressed_archive = NULL;
   
   
   if (xpackage != NULL) {
   if (xpackage != NULL) {
     const char *display_name_cstr = xpackage->Attribute("display_name");
     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;
       _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.
     // The desc file didn't include the archive file itself, weird.
     if (!freshly_downloaded) {
     if (!freshly_downloaded) {
       download_desc_file();
       download_desc_file();
@@ -368,8 +368,8 @@ got_desc_file(TiXmlDocument *doc, bool freshly_downloaded) {
     return;
     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.
   // Now get all the extractable components.
   _extracts.clear();
   _extracts.clear();
@@ -472,8 +472,6 @@ download_compressed_archive(bool allow_partial) {
     url = url.substr(0, slash + 1);
     url = url.substr(0, slash + 1);
   }
   }
   url += _compressed_archive.get_filename();
   url += _compressed_archive.get_filename();
-  cerr << "_desc_file_url = " << _desc_file_url << ", url = " << url 
-       << "\n";
 
 
   string target_pathname = _package_dir + "/" + _compressed_archive.get_filename();
   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();
   _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;
   string start_dir;
-  if (change_dir) {
+  if (!inst_mgr->get_keep_cwd()) {
     start_dir = _start_dir;
     start_dir = _start_dir;
     mkdir_complete(start_dir, nout);
     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,
 P3D_initialize(int api_version, const char *contents_filename,
                const char *download_url, bool verify_contents,
                const char *download_url, bool verify_contents,
                const char *platform,
                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) {
   if (api_version != P3D_API_VERSION) {
     // Can't accept an incompatible version.
     // Can't accept an incompatible version.
     return false;
     return false;
@@ -71,7 +72,8 @@ P3D_initialize(int api_version, const char *contents_filename,
   P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
   P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
   bool result = inst_mgr->initialize(contents_filename, download_url,
   bool result = inst_mgr->initialize(contents_filename, download_url,
                                      verify_contents, platform,
                                      verify_contents, platform,
-                                     log_directory, log_basename);
+                                     log_directory, log_basename,
+                                     keep_cwd);
   RELEASE_LOCK(_api_lock);
   RELEASE_LOCK(_api_lock);
   return result;
   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
    core API.  Note that the individual instances also have their own
    log_basename values.
    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
    This function returns true if the core API is valid and uses a
    compatible API, false otherwise.  If it returns false, the host
    compatible API, false otherwise.  If it returns false, the host
    should not call any more functions in this API, and should
    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,
 P3D_initialize_func(int api_version, const char *contents_filename,
                     const char *download_url, bool verify_contents,
                     const char *download_url, bool verify_contents,
                     const char *platform,
                     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
 /* This function should be called to unload the core API.  It will
    release all internally-allocated memory and return the core API to
    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
 #endif  // P3D_PLUGIN_P3D_PLUGIN
 
 
   nout << "Attempting to load core API from " << pathname << "\n";
   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";
     nout << "Unable to launch core API in " << pathname << "\n";
     return;
     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(),
   if (!load_plugin(pathname, contents_filename.to_os_specific(),
                    download_url, verify_contents, this_platform, _log_dirname,
                    download_url, verify_contents, this_platform, _log_dirname,
-                   _log_basename)) {
+                   _log_basename, true)) {
     cerr << "Unable to launch core API in " << pathname << "\n" << flush;
     cerr << "Unable to launch core API in " << pathname << "\n" << flush;
     return false;
     return false;
   }
   }

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

@@ -312,19 +312,48 @@ class Messenger:
             if taskChain:
             if taskChain:
                 # Queue the event onto the indicated task chain.
                 # Queue the event onto the indicated task chain.
                 from direct.task.TaskManagerGlobal import taskMgr
                 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:
             else:
                 # Handle the event immediately.
                 # Handle the event immediately.
                 self.__dispatch(acceptorDict, event, sentArgs, foundWatch)
                 self.__dispatch(acceptorDict, event, sentArgs, foundWatch)
         finally:
         finally:
             self.lock.release()
             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):
     def __dispatch(self, acceptorDict, event, sentArgs, foundWatch):
         for id in acceptorDict.keys():
         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
         # 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
         # allow the application to do it.  This is a minor hack, but
         # should work for 99% of the cases.
         # should work for 99% of the cases.
-        if self.appRunner is None:
+        if self.appRunner is None or self.appRunner.dummy:
             self.taskMgr.run()
             self.taskMgr.run()
 
 
 
 

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

@@ -161,6 +161,16 @@ class TaskManager:
             # Next time around invoke the default handler
             # Next time around invoke the default handler
             signal.signal(signal.SIGINT, self.invokeDefaultHandler)
             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,
     def setupTaskChain(self, chainName, numThreads = None, tickClock = None,
                        threadPriority = None, frameBudget = None,
                        threadPriority = None, frameBudget = None,
                        timeslicePriority = None):
                        timeslicePriority = None):