From 81363c4ba9d4b05cb0d107de2ac9522fa1c6b7cf Mon Sep 17 00:00:00 2001 From: David Rose Date: Tue, 25 Aug 2009 21:40:57 +0000 Subject: [PATCH] PackageInstaller, runtime package installation --- direct/src/gui/DirectWaitBar.py | 7 +- direct/src/p3d/AppRunner.py | 153 ++++--- direct/src/p3d/DWBPackageInstaller.py | 64 +++ direct/src/p3d/FileSpec.py | 2 +- direct/src/p3d/HostInfo.py | 39 +- direct/src/p3d/PackageInfo.py | 333 +++++++++++++- direct/src/p3d/PackageInstaller.py | 548 +++++++++++++++++++++++ direct/src/p3d/packp3d.py | 2 +- direct/src/p3d/panda3d.pdef | 2 - direct/src/plugin/load_plugin.cxx | 6 +- direct/src/plugin/load_plugin.h | 3 +- direct/src/plugin/p3dInstance.cxx | 6 - direct/src/plugin/p3dInstance.h | 1 - direct/src/plugin/p3dInstanceManager.I | 14 + direct/src/plugin/p3dInstanceManager.cxx | 5 +- direct/src/plugin/p3dInstanceManager.h | 5 +- direct/src/plugin/p3dPackage.cxx | 16 +- direct/src/plugin/p3dSession.cxx | 10 +- direct/src/plugin/p3d_plugin.cxx | 6 +- direct/src/plugin/p3d_plugin.h | 9 +- direct/src/plugin_npapi/ppInstance.cxx | 2 +- direct/src/plugin_standalone/panda3d.cxx | 2 +- direct/src/showbase/Messenger.py | 43 +- direct/src/showbase/ShowBase.py | 2 +- direct/src/task/TaskNew.py | 10 + 25 files changed, 1170 insertions(+), 120 deletions(-) create mode 100644 direct/src/p3d/DWBPackageInstaller.py create mode 100644 direct/src/p3d/PackageInstaller.py diff --git a/direct/src/gui/DirectWaitBar.py b/direct/src/gui/DirectWaitBar.py index 51ee335955..b84df687a0 100644 --- a/direct/src/gui/DirectWaitBar.py +++ b/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: diff --git a/direct/src/p3d/AppRunner.py b/direct/src/p3d/AppRunner.py index 8f05b71e90..ee52aa11b2 100644 --- a/direct/src/p3d/AppRunner.py +++ b/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 + diff --git a/direct/src/p3d/DWBPackageInstaller.py b/direct/src/p3d/DWBPackageInstaller.py new file mode 100644 index 0000000000..f347cddfd0 --- /dev/null +++ b/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() + diff --git a/direct/src/p3d/FileSpec.py b/direct/src/p3d/FileSpec.py index 4c39a957a7..0c7f67a2b0 100644 --- a/direct/src/p3d/FileSpec.py +++ b/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: diff --git a/direct/src/p3d/HostInfo.py b/direct/src/p3d/HostInfo.py index ba50c0d593..8b41420be9 100644 --- a/direct/src/p3d/HostInfo.py +++ b/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): diff --git a/direct/src/p3d/PackageInfo.py b/direct/src/p3d/PackageInfo.py index 9d2a5a27f2..6d693a82b5 100644 --- a/direct/src/p3d/PackageInfo.py +++ b/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) + + diff --git a/direct/src/p3d/PackageInstaller.py b/direct/src/p3d/PackageInstaller.py new file mode 100644 index 0000000000..68550463b8 --- /dev/null +++ b/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 + diff --git a/direct/src/p3d/packp3d.py b/direct/src/p3d/packp3d.py index afb2895fca..10a873e238 100755 --- a/direct/src/p3d/packp3d.py +++ b/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 diff --git a/direct/src/p3d/panda3d.pdef b/direct/src/p3d/panda3d.pdef index d986ea78d5..a97c1e3807 100755 --- a/direct/src/p3d/panda3d.pdef +++ b/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') diff --git a/direct/src/plugin/load_plugin.cxx b/direct/src/plugin/load_plugin.cxx index a9c258d6e0..e5b2fb3d26 100755 --- a/direct/src/plugin/load_plugin.cxx +++ b/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(); diff --git a/direct/src/plugin/load_plugin.h b/direct/src/plugin/load_plugin.h index 84229b30ba..25dbac4cb5 100755 --- a/direct/src/plugin/load_plugin.h +++ b/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(); diff --git a/direct/src/plugin/p3dInstance.cxx b/direct/src/plugin/p3dInstance.cxx index 34be343d3a..6e4a7a5b10 100644 --- a/direct/src/plugin/p3dInstance.cxx +++ b/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); diff --git a/direct/src/plugin/p3dInstance.h b/direct/src/plugin/p3dInstance.h index f7f01db299..30523c20bb 100644 --- a/direct/src/plugin/p3dInstance.h +++ b/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; diff --git a/direct/src/plugin/p3dInstanceManager.I b/direct/src/plugin/p3dInstanceManager.I index 4cb0512a16..11b3427eef 100644 --- a/direct/src/plugin/p3dInstanceManager.I +++ b/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 diff --git a/direct/src/plugin/p3dInstanceManager.cxx b/direct/src/plugin/p3dInstanceManager.cxx index 0374f9625f..2d92f6d457 100644 --- a/direct/src/plugin/p3dInstanceManager.cxx +++ b/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; diff --git a/direct/src/plugin/p3dInstanceManager.h b/direct/src/plugin/p3dInstanceManager.h index 3564b6bf8f..cd87cadfb6 100644 --- a/direct/src/plugin/p3dInstanceManager.h +++ b/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; diff --git a/direct/src/plugin/p3dPackage.cxx b/direct/src/plugin/p3dPackage.cxx index 89d686cb2a..7a065e239f 100755 --- a/direct/src/plugin/p3dPackage.cxx +++ b/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(); diff --git a/direct/src/plugin/p3dSession.cxx b/direct/src/plugin/p3dSession.cxx index 692a1e1231..ecd2e04bda 100644 --- a/direct/src/plugin/p3dSession.cxx +++ b/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); } diff --git a/direct/src/plugin/p3d_plugin.cxx b/direct/src/plugin/p3d_plugin.cxx index 3c268c8ae9..26c8160935 100644 --- a/direct/src/plugin/p3d_plugin.cxx +++ b/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; } diff --git a/direct/src/plugin/p3d_plugin.h b/direct/src/plugin/p3d_plugin.h index 75dc807f8d..a53fcf9a51 100644 --- a/direct/src/plugin/p3d_plugin.h +++ b/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 diff --git a/direct/src/plugin_npapi/ppInstance.cxx b/direct/src/plugin_npapi/ppInstance.cxx index 8597f13f4f..c1cfa79b6e 100644 --- a/direct/src/plugin_npapi/ppInstance.cxx +++ b/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; } diff --git a/direct/src/plugin_standalone/panda3d.cxx b/direct/src/plugin_standalone/panda3d.cxx index f65187bb48..090ae27d58 100644 --- a/direct/src/plugin_standalone/panda3d.cxx +++ b/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; } diff --git a/direct/src/showbase/Messenger.py b/direct/src/showbase/Messenger.py index c1b374db0d..19748a9ee9 100644 --- a/direct/src/showbase/Messenger.py +++ b/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(): diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 70f2451a65..da2fd09bcf 100644 --- a/direct/src/showbase/ShowBase.py +++ b/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() diff --git a/direct/src/task/TaskNew.py b/direct/src/task/TaskNew.py index 00866d9713..c7ac41fbea 100644 --- a/direct/src/task/TaskNew.py +++ b/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):