PackageInstaller, runtime package installation

This commit is contained in:
David Rose 2009-08-25 21:40:57 +00:00
parent 36231ac4af
commit 81363c4ba9
25 changed files with 1170 additions and 120 deletions

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

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

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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():

View File

@ -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()

View File

@ -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):