mirror of
https://github.com/panda3d/panda3d.git
synced 2025-09-30 16:58:40 -04:00
PackageInstaller, runtime package installation
This commit is contained in:
parent
36231ac4af
commit
81363c4ba9
@ -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:
|
||||
|
@ -36,6 +36,16 @@ class ScriptAttributes:
|
||||
pass
|
||||
|
||||
class AppRunner(DirectObject):
|
||||
|
||||
""" This class is intended to be compiled into the Panda3D runtime
|
||||
distributable, to execute a packaged p3d application. It also
|
||||
provides some useful runtime services while running in that
|
||||
packaged environment.
|
||||
|
||||
It does not usually exist while running Python directly, but you
|
||||
can use dummyAppRunner() to create one at startup for testing or
|
||||
development purposes. """
|
||||
|
||||
def __init__(self):
|
||||
DirectObject.__init__(self)
|
||||
|
||||
@ -45,6 +55,9 @@ class AppRunner(DirectObject):
|
||||
# child.
|
||||
sys.stdout = sys.stderr
|
||||
|
||||
# This is set true by dummyAppRunner(), below.
|
||||
self.dummy = False
|
||||
|
||||
self.sessionId = 0
|
||||
self.packedAppEnvironmentInitialized = False
|
||||
self.gotWindow = False
|
||||
@ -54,8 +67,6 @@ class AppRunner(DirectObject):
|
||||
self.windowPrc = None
|
||||
self.http = HTTPClient.getGlobalPtr()
|
||||
|
||||
self.fullDiskAccess = False
|
||||
|
||||
self.Undefined = Undefined
|
||||
self.ConcreteStruct = ConcreteStruct
|
||||
|
||||
@ -72,10 +83,18 @@ class AppRunner(DirectObject):
|
||||
# A list of the Panda3D packages that have been loaded.
|
||||
self.installedPackages = []
|
||||
|
||||
# A list of the Panda3D packages that in the queue to be
|
||||
# downloaded.
|
||||
self.downloadingPackages = []
|
||||
|
||||
# A dictionary of HostInfo objects for the various download
|
||||
# hosts we have imported packages from.
|
||||
self.hosts = {}
|
||||
|
||||
# Managing packages for runtime download.
|
||||
self.downloadingPackages = []
|
||||
self.downloadTask = None
|
||||
|
||||
# The mount point for the multifile. For now, this is always
|
||||
# the same, but when we move to multiple-instance sessions, it
|
||||
# may have to be different for each instance.
|
||||
@ -116,6 +135,36 @@ class AppRunner(DirectObject):
|
||||
# call back to the main thread.
|
||||
self.accept('AppRunner_startIfReady', self.__startIfReady)
|
||||
|
||||
def installPackage(self, packageName, version = None, hostUrl = None):
|
||||
|
||||
""" Installs the named package, downloading it first if
|
||||
necessary. Returns true on success, false on failure. This
|
||||
method runs synchronously, and will block until it is
|
||||
finished; see the PackageInstaller class if you want this to
|
||||
happen asynchronously instead. """
|
||||
|
||||
host = self.getHost(hostUrl)
|
||||
if not host.downloadContentsFile(self.http):
|
||||
return False
|
||||
|
||||
# All right, get the package info now.
|
||||
package = host.getPackage(packageName, version)
|
||||
if not package:
|
||||
print "Package %s %s not known on %s" % (
|
||||
packageName, version, hostUrl)
|
||||
return False
|
||||
|
||||
if not package.downloadDescFile(self.http):
|
||||
return False
|
||||
|
||||
if not package.downloadPackage(self.http):
|
||||
return False
|
||||
|
||||
if not package.installPackage(self):
|
||||
return False
|
||||
|
||||
print "Package %s %s installed." % (packageName, version)
|
||||
|
||||
def getHost(self, hostUrl):
|
||||
""" Returns a new HostInfo object corresponding to the
|
||||
indicated host URL. If we have already seen this URL
|
||||
@ -163,7 +212,7 @@ class AppRunner(DirectObject):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def stop(self):
|
||||
""" This method can be called by JavaScript to stop the
|
||||
application. """
|
||||
@ -191,40 +240,6 @@ class AppRunner(DirectObject):
|
||||
|
||||
vfs = VirtualFileSystem.getGlobalPtr()
|
||||
|
||||
# Unmount directories we don't need. This doesn't provide
|
||||
# actual security, since it only disables this stuff for users
|
||||
# who go through the vfs; a malicious programmer can always
|
||||
# get to the underlying true file I/O operations. Still, it
|
||||
# can help prevent honest developers from accidentally getting
|
||||
# stuck where they don't belong.
|
||||
if not self.fullDiskAccess:
|
||||
# Clear *all* the mount points, including "/", so that we
|
||||
# no longer access the disk directly.
|
||||
vfs.unmountAll()
|
||||
|
||||
# Make sure the directories on our standard Python path
|
||||
# are mounted read-only, so we can still load Python.
|
||||
# Note: read-only actually doesn't have any effect on the
|
||||
# vfs right now; careless application code can still write
|
||||
# to these directories inadvertently.
|
||||
for dirname in sys.path:
|
||||
dirname = Filename.fromOsSpecific(dirname)
|
||||
if dirname.isDirectory():
|
||||
vfs.mount(dirname, dirname, vfs.MFReadOnly)
|
||||
|
||||
# Also mount some standard directories read-write
|
||||
# (temporary and app-data directories).
|
||||
tdir = Filename.temporary('', '')
|
||||
for dirname in set([ tdir.getDirname(),
|
||||
Filename.getTempDirectory().cStr(),
|
||||
Filename.getUserAppdataDirectory().cStr(),
|
||||
Filename.getCommonAppdataDirectory().cStr() ]):
|
||||
vfs.mount(dirname, dirname, 0)
|
||||
|
||||
# And we might need the current working directory.
|
||||
dirname = ExecutionEnvironment.getCwd()
|
||||
vfs.mount(dirname, dirname, 0)
|
||||
|
||||
# Now set up Python to import this stuff.
|
||||
VFSImporter.register()
|
||||
sys.path = [ self.multifileRoot ] + sys.path
|
||||
@ -240,11 +255,6 @@ class AppRunner(DirectObject):
|
||||
os.listdir = file.listdir
|
||||
os.walk = file.walk
|
||||
|
||||
if not self.fullDiskAccess:
|
||||
# Make "/mf" our "current directory", for running the multifiles
|
||||
# we plan to mount there.
|
||||
vfs.chdir(self.multifileRoot)
|
||||
|
||||
def __startIfReady(self):
|
||||
""" Called internally to start the application. """
|
||||
if self.started:
|
||||
@ -320,7 +330,12 @@ class AppRunner(DirectObject):
|
||||
application. """
|
||||
|
||||
host = self.getHost(hostUrl)
|
||||
host.readContentsFile()
|
||||
|
||||
try:
|
||||
host.readContentsFile()
|
||||
except ValueError:
|
||||
print "Host %s has not been downloaded, cannot preload %s." % (hostUrl, name)
|
||||
return
|
||||
|
||||
if not platform:
|
||||
platform = None
|
||||
@ -328,6 +343,14 @@ class AppRunner(DirectObject):
|
||||
assert package
|
||||
self.installedPackages.append(package)
|
||||
|
||||
if package.checkStatus():
|
||||
# The package should have been loaded already. If it has,
|
||||
# go ahead and mount it.
|
||||
package.installPackage(self)
|
||||
else:
|
||||
print "%s %s is not preloaded." % (
|
||||
package.packageName, package.packageVersion)
|
||||
|
||||
def setP3DFilename(self, p3dFilename, tokens = [], argv = [],
|
||||
instanceId = None):
|
||||
""" Called by the browser to specify the p3d file that
|
||||
@ -375,36 +398,36 @@ class AppRunner(DirectObject):
|
||||
if self.p3dInfo:
|
||||
self.p3dPackage = self.p3dInfo.FirstChildElement('package')
|
||||
|
||||
if self.p3dPackage:
|
||||
fullDiskAccess = self.p3dPackage.Attribute('full_disk_access')
|
||||
try:
|
||||
self.fullDiskAccess = int(fullDiskAccess or '')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.initPackedAppEnvironment()
|
||||
|
||||
# Mount the Multifile under /mf, by convention.
|
||||
vfs.mount(mf, self.multifileRoot, vfs.MFReadOnly)
|
||||
VFSImporter.freeze_new_modules(mf, self.multifileRoot)
|
||||
|
||||
# Load any prc files in the root. We have to load them
|
||||
# explicitly, since the ConfigPageManager can't directly look
|
||||
# inside the vfs. Use the Multifile interface to find the prc
|
||||
# files, rather than vfs.scanDirectory(), so we only pick up the
|
||||
# files in this particular multifile.
|
||||
for f in mf.getSubfileNames():
|
||||
fn = Filename(f)
|
||||
if fn.getDirname() == '' and fn.getExtension() == 'prc':
|
||||
pathname = '%s/%s' % (self.multifileRoot, f)
|
||||
data = open(pathname, 'r').read()
|
||||
loadPrcFileData(pathname, data)
|
||||
|
||||
self.loadMultifilePrcFiles(mf, self.multifileRoot)
|
||||
self.gotP3DFilename = True
|
||||
|
||||
# Send this call to the main thread; don't call it directly.
|
||||
messenger.send('AppRunner_startIfReady', taskChain = 'default')
|
||||
|
||||
def loadMultifilePrcFiles(self, mf, root):
|
||||
""" Loads any prc files in the root of the indicated
|
||||
Multifile, which is presumbed to have been mounted already
|
||||
under root. """
|
||||
|
||||
# We have to load these prc files explicitly, since the
|
||||
# ConfigPageManager can't directly look inside the vfs. Use
|
||||
# the Multifile interface to find the prc files, rather than
|
||||
# vfs.scanDirectory(), so we only pick up the files in this
|
||||
# particular multifile.
|
||||
for f in mf.getSubfileNames():
|
||||
fn = Filename(f)
|
||||
if fn.getDirname() == '' and fn.getExtension() == 'prc':
|
||||
pathname = '%s/%s' % (root, f)
|
||||
data = open(pathname, 'r').read()
|
||||
loadPrcFileData(pathname, data)
|
||||
|
||||
|
||||
def __clearWindowPrc(self):
|
||||
""" Clears the windowPrc file that was created in a previous
|
||||
call to setupWindow(), if any. """
|
||||
@ -563,7 +586,7 @@ class AppRunner(DirectObject):
|
||||
|
||||
self.sendRequest('drop_p3dobj', objectId)
|
||||
|
||||
def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
|
||||
def dummyAppRunner(tokens = [], argv = None):
|
||||
""" This function creates a dummy global AppRunner object, which
|
||||
is useful for testing running in a packaged environment without
|
||||
actually bothering to package up the application. Call this at
|
||||
@ -580,6 +603,7 @@ def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
|
||||
return
|
||||
|
||||
appRunner = AppRunner()
|
||||
appRunner.dummy = True
|
||||
AppRunnerGlobal.appRunner = appRunner
|
||||
|
||||
platform = PandaSystem.getPlatform()
|
||||
@ -604,7 +628,6 @@ def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
|
||||
|
||||
appRunner.p3dInfo = None
|
||||
appRunner.p3dPackage = None
|
||||
appRunner.fullDiskAccess = fullDiskAccess
|
||||
|
||||
# Mount the current directory under the multifileRoot, as if it
|
||||
# were coming from a multifile.
|
||||
@ -619,3 +642,5 @@ def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
|
||||
os.listdir = file.listdir
|
||||
os.walk = file.walk
|
||||
|
||||
return appRunner
|
||||
|
||||
|
64
direct/src/p3d/DWBPackageInstaller.py
Normal file
64
direct/src/p3d/DWBPackageInstaller.py
Normal 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()
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -1,4 +1,7 @@
|
||||
from pandac.PandaModules import Filename
|
||||
from pandac.PandaModules import Filename, URLSpec, DocumentSpec, Ramfile, TiXmlDocument, Multifile, Decompressor, EUOk, EUSuccess, VirtualFileSystem, Thread
|
||||
from direct.p3d.FileSpec import FileSpec
|
||||
import os
|
||||
import sys
|
||||
|
||||
class PackageInfo:
|
||||
|
||||
@ -12,11 +15,333 @@ class PackageInfo:
|
||||
self.packageVersion = packageVersion
|
||||
self.platform = platform
|
||||
|
||||
self.packageFullname = '%s.%s' % (self.packageName, self.packageVersion)
|
||||
self.packageDir = Filename(host.hostDir, 'packages/%s/%s' % (self.packageName, self.packageVersion))
|
||||
self.descFileBasename = self.packageFullname + '.xml'
|
||||
self.packageDir = Filename(host.hostDir, '%s/%s' % (self.packageName, self.packageVersion))
|
||||
|
||||
# These will be filled in by HostInfo when the package is read
|
||||
# from contents.xml.
|
||||
self.descFileUrl = None
|
||||
self.descFile = None
|
||||
self.importDescFile = None
|
||||
|
||||
# These are filled in when the desc file is successfully read.
|
||||
self.hasDescFile = False
|
||||
self.displayName = None
|
||||
self.uncompressedArchive = None
|
||||
self.compressedArchive = None
|
||||
self.extracts = []
|
||||
|
||||
# These are incremented during downloadPackage().
|
||||
self.bytesDownloaded = 0
|
||||
self.bytesUncompressed = 0
|
||||
self.bytesUnpacked = 0
|
||||
|
||||
# This is set true when the package file has been fully
|
||||
# downloaded and unpackaged.
|
||||
self.hasPackage = False
|
||||
|
||||
def getDownloadSize(self):
|
||||
""" Returns the number of bytes we will need to download in
|
||||
order to install this package. """
|
||||
if self.hasPackage:
|
||||
return 0
|
||||
return self.compressedArchive.size
|
||||
|
||||
def getUncompressSize(self):
|
||||
""" Returns the number of bytes we will need to uncompress in
|
||||
order to install this package. """
|
||||
if self.hasPackage:
|
||||
return 0
|
||||
return self.uncompressedArchive.size
|
||||
|
||||
def getUnpackSize(self):
|
||||
""" Returns the number of bytes that we will need to unpack
|
||||
when installing the package. """
|
||||
|
||||
if self.hasPackage:
|
||||
return 0
|
||||
|
||||
size = 0
|
||||
for file in self.extracts:
|
||||
size += file.size
|
||||
return size
|
||||
|
||||
def setupFilenames(self):
|
||||
""" This is called by the HostInfo when the package is read
|
||||
from contents.xml, to set up the internal filenames and such
|
||||
that rely on some of the information from contents.xml. """
|
||||
|
||||
self.descFileUrl = self.host.hostUrlPrefix + self.descFile.filename
|
||||
|
||||
basename = self.descFile.filename.rsplit('/', 1)[-1]
|
||||
self.descFileBasename = basename
|
||||
|
||||
def checkStatus(self):
|
||||
""" Checks the current status of the desc file and the package
|
||||
contents on disk. """
|
||||
|
||||
if self.hasPackage:
|
||||
return True
|
||||
|
||||
if not self.hasDescFile:
|
||||
filename = Filename(self.packageDir, self.descFileBasename)
|
||||
if self.descFile.quickVerify(self.packageDir, pathname = filename):
|
||||
self.readDescFile()
|
||||
|
||||
if self.hasDescFile:
|
||||
if self.__checkArchiveStatus():
|
||||
# It's all good.
|
||||
self.hasPackage = True
|
||||
|
||||
return self.hasPackage
|
||||
|
||||
def downloadDescFile(self, http):
|
||||
""" Downloads the desc file for this particular package,
|
||||
synchronously, and then reads it. Returns true on success,
|
||||
false on failure. """
|
||||
|
||||
assert self.descFile
|
||||
|
||||
if self.hasDescFile:
|
||||
# We've already got one.
|
||||
return True
|
||||
|
||||
url = URLSpec(self.descFileUrl)
|
||||
print "Downloading %s" % (url)
|
||||
|
||||
rf = Ramfile()
|
||||
channel = http.getDocument(url)
|
||||
if not channel.downloadToRam(rf):
|
||||
print "Unable to download %s" % (url)
|
||||
return False
|
||||
|
||||
filename = Filename(self.packageDir, self.descFileBasename)
|
||||
filename.makeDir()
|
||||
f = open(filename.toOsSpecific(), 'wb')
|
||||
f.write(rf.getData())
|
||||
f.close()
|
||||
|
||||
try:
|
||||
self.readDescFile()
|
||||
except ValueError:
|
||||
print "Failure reading %s" % (filename)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def readDescFile(self):
|
||||
""" Reads the desc xml file for this particular package.
|
||||
Presumably this has already been downloaded and installed. """
|
||||
|
||||
if self.hasDescFile:
|
||||
# No need to read it again.
|
||||
return
|
||||
|
||||
filename = Filename(self.packageDir, self.descFileBasename)
|
||||
|
||||
doc = TiXmlDocument(filename.toOsSpecific())
|
||||
if not doc.LoadFile():
|
||||
raise ValueError
|
||||
|
||||
xpackage = doc.FirstChildElement('package')
|
||||
if not xpackage:
|
||||
raise ValueError
|
||||
|
||||
# The name for display to an English-speaking user.
|
||||
self.displayName = xpackage.Attribute('display_name')
|
||||
|
||||
# The uncompressed archive, which will be mounted directly,
|
||||
# and also used for patching.
|
||||
xuncompressedArchive = xpackage.FirstChildElement('uncompressed_archive')
|
||||
if xuncompressedArchive:
|
||||
self.uncompressedArchive = FileSpec()
|
||||
self.uncompressedArchive.loadXml(xuncompressedArchive)
|
||||
|
||||
# The compressed archive, which is what is downloaded.
|
||||
xcompressedArchive = xpackage.FirstChildElement('compressed_archive')
|
||||
if xcompressedArchive:
|
||||
self.compressedArchive = FileSpec()
|
||||
self.compressedArchive.loadXml(xcompressedArchive)
|
||||
|
||||
# The list of files that should be extracted to disk.
|
||||
xextract = xpackage.FirstChildElement('extract')
|
||||
while xextract:
|
||||
file = FileSpec()
|
||||
file.loadXml(xextract)
|
||||
self.extracts.append(file)
|
||||
xextract = xextract.NextSiblingElement('extract')
|
||||
|
||||
self.hasDescFile = True
|
||||
|
||||
# Now that we've read the desc file, go ahead and use it to
|
||||
# verify the download status.
|
||||
if self.__checkArchiveStatus():
|
||||
# It's all good.
|
||||
self.hasPackage = True
|
||||
return
|
||||
|
||||
# We need to download an update.
|
||||
self.hasPackage = False
|
||||
|
||||
def __checkArchiveStatus(self):
|
||||
""" Returns true if the archive and all extractable files are
|
||||
already correct on disk, false otherwise. """
|
||||
|
||||
allExtractsOk = True
|
||||
if not self.uncompressedArchive.quickVerify(self.packageDir):
|
||||
print "File is incorrect: %s" % (self.uncompressedArchive.filename)
|
||||
allExtractsOk = False
|
||||
|
||||
if allExtractsOk:
|
||||
for file in self.extracts:
|
||||
if not file.quickVerify(self.packageDir):
|
||||
print "File is incorrect: %s" % (file.filename)
|
||||
allExtractsOk = False
|
||||
break
|
||||
|
||||
return allExtractsOk
|
||||
|
||||
def downloadPackage(self, http):
|
||||
""" Downloads the package file, synchronously, then
|
||||
uncompresses and unpacks it. Returns true on success, false
|
||||
on failure. """
|
||||
|
||||
assert self.hasDescFile
|
||||
|
||||
if self.hasPackage:
|
||||
# We've already got one.
|
||||
return True
|
||||
|
||||
if self.uncompressedArchive.quickVerify(self.packageDir):
|
||||
return self.__unpackArchive()
|
||||
|
||||
if self.compressedArchive.quickVerify(self.packageDir):
|
||||
return self.__uncompressArchive()
|
||||
|
||||
url = self.descFileUrl.rsplit('/', 1)[0]
|
||||
url += '/' + self.compressedArchive.filename
|
||||
url = DocumentSpec(url)
|
||||
print "Downloading %s" % (url)
|
||||
|
||||
targetPathname = Filename(self.packageDir, self.compressedArchive.filename)
|
||||
targetPathname.setBinary()
|
||||
|
||||
channel = http.makeChannel(False)
|
||||
channel.beginGetDocument(url)
|
||||
channel.downloadToFile(targetPathname)
|
||||
while channel.run():
|
||||
self.bytesDownloaded = channel.getBytesDownloaded()
|
||||
Thread.considerYield()
|
||||
self.bytesDownloaded = channel.getBytesDownloaded()
|
||||
if not channel.isValid():
|
||||
print "Failed to download %s" % (url)
|
||||
return False
|
||||
|
||||
if not self.compressedArchive.fullVerify(self.packageDir):
|
||||
print "after downloading, %s still incorrect" % (
|
||||
self.compressedArchive.filename)
|
||||
return False
|
||||
|
||||
return self.__uncompressArchive()
|
||||
|
||||
def __uncompressArchive(self):
|
||||
""" Turns the compressed archive into the uncompressed
|
||||
archive, then unpacks it. Returns true on success, false on
|
||||
failure. """
|
||||
|
||||
sourcePathname = Filename(self.packageDir, self.compressedArchive.filename)
|
||||
targetPathname = Filename(self.packageDir, self.uncompressedArchive.filename)
|
||||
|
||||
print sourcePathname, targetPathname
|
||||
decompressor = Decompressor()
|
||||
decompressor.initiate(sourcePathname, targetPathname)
|
||||
totalBytes = self.uncompressedArchive.size
|
||||
result = decompressor.run()
|
||||
while result == EUOk:
|
||||
self.bytesUncompressed = int(totalBytes * decompressor.getProgress())
|
||||
result = decompressor.run()
|
||||
Thread.considerYield()
|
||||
|
||||
if result != EUSuccess:
|
||||
return False
|
||||
|
||||
self.bytesUncompressed = totalBytes
|
||||
|
||||
if not self.uncompressedArchive.quickVerify(self.packageDir):
|
||||
print "after uncompressing, %s still incorrect" % (
|
||||
self.uncompressedArchive.filename)
|
||||
return False
|
||||
|
||||
return self.__unpackArchive()
|
||||
|
||||
def __unpackArchive(self):
|
||||
""" Unpacks any files in the archive that want to be unpacked
|
||||
to disk. """
|
||||
|
||||
if not self.extracts:
|
||||
# Nothing to extract.
|
||||
self.hasPackage = True
|
||||
return True
|
||||
|
||||
mfPathname = Filename(self.packageDir, self.uncompressedArchive.filename)
|
||||
mf = Multifile()
|
||||
if not mf.openRead(mfPathname):
|
||||
print "Couldn't open %s" % (mfPathname)
|
||||
return False
|
||||
|
||||
allExtractsOk = True
|
||||
self.bytesUnpacked = 0
|
||||
for file in self.extracts:
|
||||
i = mf.findSubfile(file.filename)
|
||||
if i == -1:
|
||||
print "Not in Multifile: %s" % (file.filename)
|
||||
allExtractsOk = False
|
||||
continue
|
||||
|
||||
targetPathname = Filename(self.packageDir, file.filename)
|
||||
if not mf.extractSubfile(i, targetPathname):
|
||||
print "Couldn't extract: %s" % (file.filename)
|
||||
allExtractsOk = False
|
||||
continue
|
||||
|
||||
if not file.quickVerify(self.packageDir):
|
||||
print "After extracting, still incorrect: %s" % (file.filename)
|
||||
allExtractsOk = False
|
||||
continue
|
||||
|
||||
# Make sure it's executable.
|
||||
os.chmod(targetPathname.toOsSpecific(), 0755)
|
||||
|
||||
self.bytesUnpacked += file.size
|
||||
Thread.considerYield()
|
||||
|
||||
if not allExtractsOk:
|
||||
return False
|
||||
|
||||
self.hasPackage = True
|
||||
return True
|
||||
|
||||
def installPackage(self, appRunner):
|
||||
""" Mounts the package and sets up system paths so it becomes
|
||||
available for use. """
|
||||
|
||||
assert self.hasPackage
|
||||
|
||||
mfPathname = Filename(self.packageDir, self.uncompressedArchive.filename)
|
||||
mf = Multifile()
|
||||
if not mf.openRead(mfPathname):
|
||||
print "Couldn't open %s" % (mfPathname)
|
||||
return False
|
||||
|
||||
# We mount it under its actual location on disk.
|
||||
root = self.packageDir.cStr()
|
||||
|
||||
vfs = VirtualFileSystem.getGlobalPtr()
|
||||
vfs.mount(mf, root, vfs.MFReadOnly)
|
||||
|
||||
appRunner.loadMultifilePrcFiles(mf, root)
|
||||
|
||||
if root not in sys.path:
|
||||
sys.path.append(root)
|
||||
print "Installed %s %s" % (self.packageName, self.packageVersion)
|
||||
|
||||
|
||||
|
548
direct/src/p3d/PackageInstaller.py
Normal file
548
direct/src/p3d/PackageInstaller.py
Normal 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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
|
@ -160,7 +160,6 @@ private:
|
||||
string _session_key;
|
||||
string _python_version;
|
||||
string _log_basename;
|
||||
bool _full_disk_access;
|
||||
bool _hidden;
|
||||
|
||||
P3DSession *_session;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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():
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user