mirror of
https://github.com/Sneed-Group/Poodletooth-iLand
synced 2024-12-24 20:22:33 -06:00
1279 lines
48 KiB
Python
1279 lines
48 KiB
Python
|
|
"""
|
|
|
|
This module is intended to be compiled into the Panda3D runtime
|
|
distributable, to execute a packaged p3d application, but it can also
|
|
be run directly via the Python interpreter (if the current Panda3D and
|
|
Python versions match the version expected by the application). See
|
|
runp3d.py for a command-line tool to invoke this module.
|
|
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import types
|
|
import __builtin__
|
|
|
|
if 'VFSImporter' in sys.modules:
|
|
# If we've already got a VFSImporter module defined at the
|
|
# toplevel, we must have come in here by way of the
|
|
# p3dPythonRun.cxx program, which starts out by importing a frozen
|
|
# VFSImporter. Let's make sure we don't have two VFSImporter
|
|
# modules.
|
|
import VFSImporter
|
|
import direct.showbase
|
|
direct.showbase.VFSImporter = VFSImporter
|
|
sys.modules['direct.showbase.VFSImporter'] = VFSImporter
|
|
else:
|
|
# Otherwise, we can import the VFSImporter normally. We have to
|
|
# import PandaModules first, to get the funny renaming with
|
|
# pandaexpress.
|
|
import direct
|
|
from pandac import PandaModules
|
|
from direct.showbase import VFSImporter
|
|
|
|
from direct.showbase.DirectObject import DirectObject
|
|
from pandac.PandaModules import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, Thread, WindowProperties, ExecutionEnvironment, PandaSystem, Notify, StreamWriter, ConfigVariableString, ConfigPageManager, initAppForGui
|
|
from pandac import PandaModules
|
|
from direct.stdpy import file, glob
|
|
from direct.task.TaskManagerGlobal import taskMgr
|
|
from direct.showbase.MessengerGlobal import messenger
|
|
from direct.showbase import AppRunnerGlobal
|
|
from direct.directnotify.DirectNotifyGlobal import directNotify
|
|
from direct.p3d.HostInfo import HostInfo
|
|
from direct.p3d.ScanDirectoryNode import ScanDirectoryNode
|
|
from direct.p3d.InstalledHostData import InstalledHostData
|
|
from direct.p3d.InstalledPackageData import InstalledPackageData
|
|
|
|
# These imports are read by the C++ wrapper in p3dPythonRun.cxx.
|
|
from direct.p3d.JavaScript import UndefinedObject, Undefined, ConcreteStruct, BrowserObject
|
|
|
|
class ArgumentError(AttributeError):
|
|
pass
|
|
|
|
class ScriptAttributes:
|
|
""" This dummy class serves as the root object for the scripting
|
|
interface. The Python code can store objects and functions here
|
|
for direct inspection by the browser's JavaScript code. """
|
|
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. """
|
|
|
|
notify = directNotify.newCategory("AppRunner")
|
|
|
|
ConfigBasename = 'config.xml'
|
|
|
|
# Default values for parameters that are absent from the config file:
|
|
maxDiskUsage = 2048 * 1048576 # 2 GB
|
|
|
|
# Values for verifyContents, from p3d_plugin.h
|
|
P3DVCNone = 0
|
|
P3DVCNormal = 1
|
|
P3DVCForce = 2
|
|
P3DVCNever = 3
|
|
|
|
# Also from p3d_plugin.h
|
|
P3D_CONTENTS_DEFAULT_MAX_AGE = 5
|
|
|
|
def __init__(self):
|
|
DirectObject.__init__(self)
|
|
|
|
# We direct both our stdout and stderr objects onto Panda's
|
|
# Notify stream. This ensures that unadorned print statements
|
|
# made within Python will get routed into the log properly.
|
|
stream = StreamWriter(Notify.out(), False)
|
|
sys.stdout = stream
|
|
sys.stderr = stream
|
|
|
|
# This is set true by dummyAppRunner(), below.
|
|
self.dummy = False
|
|
|
|
# These will be set from the application flags when
|
|
# setP3DFilename() is called.
|
|
self.allowPythonDev = False
|
|
self.guiApp = False
|
|
self.interactiveConsole = False
|
|
self.initialAppImport = False
|
|
self.trueFileIO = False
|
|
self.respectPerPlatform = None
|
|
|
|
self.verifyContents = self.P3DVCNone
|
|
|
|
self.sessionId = 0
|
|
self.packedAppEnvironmentInitialized = False
|
|
self.gotWindow = False
|
|
self.gotP3DFilename = False
|
|
self.p3dFilename = None
|
|
self.p3dUrl = None
|
|
self.started = False
|
|
self.windowOpened = False
|
|
self.windowPrc = None
|
|
|
|
self.http = None
|
|
if hasattr(PandaModules, 'HTTPClient'):
|
|
self.http = PandaModules.HTTPClient.getGlobalPtr()
|
|
|
|
self.Undefined = Undefined
|
|
self.ConcreteStruct = ConcreteStruct
|
|
|
|
# This is per session.
|
|
self.nextScriptId = 0
|
|
|
|
# TODO: we need one of these per instance, not per session.
|
|
self.instanceId = None
|
|
|
|
# The root Panda3D install directory. This is filled in when
|
|
# the instance starts up.
|
|
self.rootDir = None
|
|
|
|
# The log directory. Also filled in when the instance starts.
|
|
self.logDirectory = None
|
|
|
|
# self.superMirrorUrl, if nonempty, is the "super mirror" URL
|
|
# that should be contacted first before trying the actual
|
|
# host. This is primarily used for "downloading" from a
|
|
# locally-stored Panda3D installation. This is also filled in
|
|
# when the instance starts up.
|
|
self.superMirrorUrl = None
|
|
|
|
# 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 = {}
|
|
|
|
# The altHost string that is in effect from the HTML tokens,
|
|
# if any, and the dictionary of URL remapping: orig host url
|
|
# -> alt host url.
|
|
self.altHost = None
|
|
self.altHostMap = {}
|
|
|
|
# The URL from which Panda itself should be downloaded.
|
|
self.pandaHostUrl = PandaSystem.getPackageHostUrl()
|
|
|
|
# Application code can assign a callable object here; if so,
|
|
# it will be invoked when an uncaught exception propagates to
|
|
# the top of the TaskMgr.run() loop.
|
|
self.exceptionHandler = None
|
|
|
|
# Managing packages for runtime download.
|
|
self.downloadingPackages = []
|
|
self.downloadTask = None
|
|
|
|
# The mount point for the multifile. For now, this is always
|
|
# the current working directory, for convenience; but when we
|
|
# move to multiple-instance sessions, it may have to be
|
|
# different for each instance.
|
|
self.multifileRoot = ExecutionEnvironment.getCwd().cStr()
|
|
|
|
# The "main" object will be exposed to the DOM as a property
|
|
# of the plugin object; that is, document.pluginobject.main in
|
|
# JavaScript will be appRunner.main here. This may be
|
|
# replaced with a direct reference to the JavaScript object
|
|
# later, in setInstanceInfo().
|
|
self.main = ScriptAttributes()
|
|
|
|
# By default, we publish a stop() method so the browser can
|
|
# easy stop the plugin. A particular application can remove
|
|
# this if it chooses.
|
|
self.main.stop = self.stop
|
|
|
|
# This will be the browser's toplevel window DOM object;
|
|
# e.g. self.dom.document will be the document.
|
|
self.dom = None
|
|
|
|
# This is the list of expressions we will evaluate when
|
|
# self.dom gets assigned.
|
|
self.deferredEvals = []
|
|
|
|
# This is the default requestFunc that is installed if we
|
|
# never call setRequestFunc().
|
|
def defaultRequestFunc(*args):
|
|
if args[1] == 'notify':
|
|
# Quietly ignore notifies.
|
|
return
|
|
self.notify.info("Ignoring request: %s" % (args,))
|
|
self.requestFunc = defaultRequestFunc
|
|
|
|
# This will be filled in with the default WindowProperties for
|
|
# this instance, e.g. the WindowProperties necessary to
|
|
# re-embed a window in the browser frame.
|
|
self.windowProperties = None
|
|
|
|
# Store our pointer so DirectStart-based apps can find us.
|
|
if AppRunnerGlobal.appRunner is None:
|
|
AppRunnerGlobal.appRunner = self
|
|
|
|
# We use this messenger hook to dispatch this __startIfReady()
|
|
# call back to the main thread.
|
|
self.accept('AppRunner_startIfReady', self.__startIfReady)
|
|
|
|
def getToken(self, tokenName):
|
|
""" Returns the value of the indicated web token as a string,
|
|
if it was set, or None if it was not. """
|
|
|
|
return self.tokenDict.get(tokenName.lower(), None)
|
|
|
|
def getTokenInt(self, tokenName):
|
|
""" Returns the value of the indicated web token as an integer
|
|
value, if it was set, or None if it was not, or not an
|
|
integer. """
|
|
|
|
value = self.getToken(tokenName)
|
|
if value is not None:
|
|
try:
|
|
value = int(value)
|
|
except ValueError:
|
|
value = None
|
|
return value
|
|
|
|
def getTokenFloat(self, tokenName):
|
|
""" Returns the value of the indicated web token as a
|
|
floating-point value value, if it was set, or None if it was
|
|
not, or not a number. """
|
|
|
|
value = self.getToken(tokenName)
|
|
if value is not None:
|
|
try:
|
|
value = float(value)
|
|
except ValueError:
|
|
value = None
|
|
return value
|
|
|
|
def getTokenBool(self, tokenName):
|
|
""" Returns the value of the indicated web token as a boolean
|
|
value, if it was set, or None if it was not. """
|
|
|
|
value = self.getTokenInt(tokenName)
|
|
if value is not None:
|
|
value = bool(value)
|
|
return value
|
|
|
|
|
|
|
|
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.getHostWithAlt(hostUrl)
|
|
if not host.downloadContentsFile(self.http):
|
|
return False
|
|
|
|
# All right, get the package info now.
|
|
package = host.getPackage(packageName, version)
|
|
if not package:
|
|
self.notify.warning("Package %s %s not known on %s" % (
|
|
packageName, version, hostUrl))
|
|
return False
|
|
|
|
return self.__rInstallPackage(package, [])
|
|
|
|
def __rInstallPackage(self, package, nested):
|
|
""" The recursive implementation of installPackage(). The new
|
|
parameter, nested, is a list of packages that we are
|
|
recursively calling this from, to avoid recursive loops. """
|
|
|
|
package.checkStatus()
|
|
if not package.downloadDescFile(self.http):
|
|
return False
|
|
|
|
# Now that we've downloaded and read the desc file, we can
|
|
# install all of the required packages first.
|
|
nested = nested[:] + [self]
|
|
for packageName, version, host in package.requires:
|
|
if host.downloadContentsFile(self.http):
|
|
p2 = host.getPackage(packageName, version)
|
|
if not p2:
|
|
self.notify.warning("Couldn't find %s %s on %s" % (packageName, version, host.hostUrl))
|
|
else:
|
|
if p2 not in nested:
|
|
self.__rInstallPackage(p2, nested)
|
|
|
|
# Now that all of the required packages are installed, carry
|
|
# on to download and install this package.
|
|
if not package.downloadPackage(self.http):
|
|
return False
|
|
|
|
if not package.installPackage(self):
|
|
return False
|
|
|
|
self.notify.info("Package %s %s installed." % (
|
|
package.packageName, package.packageVersion))
|
|
return True
|
|
|
|
def getHostWithAlt(self, hostUrl):
|
|
""" Returns a suitable HostInfo object for downloading
|
|
contents from the indicated URL. This is almost always the
|
|
same thing as getHost(), except in the rare case when we have
|
|
an alt_host specified in the HTML tokens; in this case, we may
|
|
actually want to download the contents from a different URL
|
|
than the one given, for instance to download a version in
|
|
testing. """
|
|
|
|
if hostUrl is None:
|
|
hostUrl = self.pandaHostUrl
|
|
|
|
altUrl = self.altHostMap.get(hostUrl, None)
|
|
if altUrl:
|
|
# We got an alternate host. Use it.
|
|
return self.getHost(altUrl)
|
|
|
|
# We didn't get an aternate host, use the original.
|
|
host = self.getHost(hostUrl)
|
|
|
|
# But we might need to consult the host itself to see if *it*
|
|
# recommends an altHost.
|
|
if self.altHost:
|
|
# This means forcing the host to download its contents
|
|
# file on the spot, a blocking operation. This is a
|
|
# little unfortunate, but since alt_host is so rarely
|
|
# used, probably not really a problem.
|
|
host.downloadContentsFile(self.http)
|
|
altUrl = host.altHosts.get(self.altHost, None)
|
|
if altUrl:
|
|
return self.getHost(altUrl)
|
|
|
|
# No shenanigans, just return the requested host.
|
|
return host
|
|
|
|
def getHost(self, hostUrl, hostDir = None):
|
|
""" Returns a new HostInfo object corresponding to the
|
|
indicated host URL. If we have already seen this URL
|
|
previously, returns the same object.
|
|
|
|
This returns the literal referenced host. To return the
|
|
mapped host, which is the one we should actually download
|
|
from, see getHostWithAlt(). """
|
|
|
|
if not hostUrl:
|
|
hostUrl = self.pandaHostUrl
|
|
|
|
host = self.hosts.get(hostUrl, None)
|
|
if not host:
|
|
host = HostInfo(hostUrl, appRunner = self, hostDir = hostDir)
|
|
self.hosts[hostUrl] = host
|
|
return host
|
|
|
|
def getHostWithDir(self, hostDir):
|
|
""" Returns the HostInfo object that corresponds to the
|
|
indicated on-disk host directory. This would be used when
|
|
reading a host directory from disk, instead of downloading it
|
|
from a server. Supply the full path to the host directory, as
|
|
a Filename. Returns None if the contents.xml in the indicated
|
|
host directory cannot be read or doesn't seem consistent. """
|
|
|
|
host = HostInfo(None, hostDir = hostDir, appRunner = self)
|
|
if not host.hasContentsFile:
|
|
if not host.readContentsFile():
|
|
# Couldn't read the contents.xml file
|
|
return None
|
|
|
|
if not host.hostUrl:
|
|
# The contents.xml file there didn't seem to indicate the
|
|
# same host directory.
|
|
return None
|
|
|
|
host2 = self.hosts.get(host.hostUrl)
|
|
if host2 is None:
|
|
# No such host already; store this one.
|
|
self.hosts[host.hostUrl] = host
|
|
return host
|
|
|
|
if host2.hostDir != host.hostDir:
|
|
# Hmm, we already have that host somewhere else.
|
|
return None
|
|
|
|
# We already have that host, and it's consistent.
|
|
return host2
|
|
|
|
def deletePackages(self, packages):
|
|
""" Removes all of the indicated packages from the disk,
|
|
uninstalling them and deleting all of their files. The
|
|
packages parameter must be a list of one or more PackageInfo
|
|
objects, for instance as returned by getHost().getPackage().
|
|
Returns the list of packages that were NOT found. """
|
|
|
|
for hostUrl, host in self.hosts.items():
|
|
packages = host.deletePackages(packages)
|
|
|
|
if not host.packages:
|
|
# If that's all of the packages for this host, delete
|
|
# the host directory too.
|
|
del self.hosts[hostUrl]
|
|
self.__deleteHostFiles(host)
|
|
|
|
return packages
|
|
|
|
def __deleteHostFiles(self, host):
|
|
""" Called by deletePackages(), this removes all the files for
|
|
the indicated host (for which we have presumably already
|
|
removed all of the packages). """
|
|
|
|
self.notify.info("Deleting host %s: %s" % (host.hostUrl, host.hostDir))
|
|
self.rmtree(host.hostDir)
|
|
|
|
self.sendRequest('forget_package', host.hostUrl, '', '')
|
|
|
|
|
|
def freshenFile(self, host, fileSpec, localPathname):
|
|
""" Ensures that the localPathname is the most current version
|
|
of the file defined by fileSpec, as offered by host. If not,
|
|
it downloads a new version on-the-spot. Returns true on
|
|
success, false on failure. """
|
|
|
|
if fileSpec.quickVerify(pathname = localPathname):
|
|
# It's good, keep it.
|
|
return True
|
|
|
|
assert self.http
|
|
|
|
# It's stale, get a new one.
|
|
doc = None
|
|
if self.superMirrorUrl:
|
|
# Use the "super mirror" first.
|
|
url = PandaModules.URLSpec(self.superMirrorUrl + fileSpec.filename)
|
|
self.notify.info("Freshening %s" % (url))
|
|
doc = self.http.getDocument(url)
|
|
|
|
if not doc or not doc.isValid():
|
|
# Failing the super mirror, contact the actual host.
|
|
url = PandaModules.URLSpec(host.hostUrlPrefix + fileSpec.filename)
|
|
self.notify.info("Freshening %s" % (url))
|
|
doc = self.http.getDocument(url)
|
|
if not doc.isValid():
|
|
return False
|
|
|
|
file = Filename.temporary('', 'p3d_')
|
|
if not doc.downloadToFile(file):
|
|
# Failed to download.
|
|
file.unlink()
|
|
return False
|
|
|
|
# Successfully downloaded!
|
|
localPathname.makeDir()
|
|
if not file.renameTo(localPathname):
|
|
# Couldn't move it into place.
|
|
file.unlink()
|
|
return False
|
|
|
|
if not fileSpec.fullVerify(pathname = localPathname):
|
|
# No good after download.
|
|
self.notify.info("%s is still no good after downloading." % (url))
|
|
return False
|
|
|
|
return True
|
|
|
|
def scanInstalledPackages(self):
|
|
""" Scans the hosts and packages already installed locally on
|
|
the system. Returns a list of InstalledHostData objects, each
|
|
of which contains a list of InstalledPackageData objects. """
|
|
|
|
result = []
|
|
hostsFilename = Filename(self.rootDir, 'hosts')
|
|
hostsDir = ScanDirectoryNode(hostsFilename)
|
|
for dirnode in hostsDir.nested:
|
|
host = self.getHostWithDir(dirnode.pathname)
|
|
hostData = InstalledHostData(host, dirnode)
|
|
|
|
if host:
|
|
for package in host.getAllPackages(includeAllPlatforms = True):
|
|
packageDir = package.getPackageDir()
|
|
if not packageDir.exists():
|
|
continue
|
|
|
|
subdir = dirnode.extractSubdir(packageDir)
|
|
if not subdir:
|
|
# This package, while defined by the host, isn't installed
|
|
# locally; ignore it.
|
|
continue
|
|
|
|
packageData = InstalledPackageData(package, subdir)
|
|
hostData.packages.append(packageData)
|
|
|
|
# Now that we've examined all of the packages for the host,
|
|
# anything left over is junk.
|
|
for subdir in dirnode.nested:
|
|
packageData = InstalledPackageData(None, subdir)
|
|
hostData.packages.append(packageData)
|
|
|
|
result.append(hostData)
|
|
|
|
return result
|
|
|
|
def readConfigXml(self):
|
|
""" Reads the config.xml file that may be present in the root
|
|
directory. """
|
|
|
|
if not hasattr(PandaModules, 'TiXmlDocument'):
|
|
return
|
|
from pandac.PandaModules import TiXmlDocument
|
|
|
|
filename = Filename(self.rootDir, self.ConfigBasename)
|
|
doc = TiXmlDocument(filename.toOsSpecific())
|
|
if not doc.LoadFile():
|
|
return
|
|
|
|
xconfig = doc.FirstChildElement('config')
|
|
if xconfig:
|
|
maxDiskUsage = xconfig.Attribute('max_disk_usage')
|
|
try:
|
|
self.maxDiskUsage = int(maxDiskUsage or '')
|
|
except ValueError:
|
|
pass
|
|
|
|
def writeConfigXml(self):
|
|
""" Rewrites the config.xml to the root directory. This isn't
|
|
called automatically; an application may call this after
|
|
adjusting some parameters (such as self.maxDiskUsage). """
|
|
|
|
from pandac.PandaModules import TiXmlDocument, TiXmlDeclaration, TiXmlElement
|
|
|
|
filename = Filename(self.rootDir, self.ConfigBasename)
|
|
doc = TiXmlDocument(filename.toOsSpecific())
|
|
decl = TiXmlDeclaration("1.0", "utf-8", "")
|
|
doc.InsertEndChild(decl)
|
|
|
|
xconfig = TiXmlElement('config')
|
|
xconfig.SetAttribute('max_disk_usage', str(self.maxDiskUsage))
|
|
doc.InsertEndChild(xconfig)
|
|
|
|
# Write the file to a temporary filename, then atomically move
|
|
# it to its actual filename, to avoid race conditions when
|
|
# updating this file.
|
|
tfile = Filename.temporary(self.rootDir.cStr(), '.xml')
|
|
if doc.SaveFile(tfile.toOsSpecific()):
|
|
tfile.renameTo(filename)
|
|
|
|
|
|
def checkDiskUsage(self):
|
|
""" Checks the total disk space used by all packages, and
|
|
removes old packages if necessary. """
|
|
|
|
totalSize = 0
|
|
hosts = self.scanInstalledPackages()
|
|
for hostData in hosts:
|
|
for packageData in hostData.packages:
|
|
totalSize += packageData.totalSize
|
|
self.notify.info("Total Panda3D disk space used: %s MB" % (
|
|
(totalSize + 524288) / 1048576))
|
|
|
|
if self.verifyContents == self.P3DVCNever:
|
|
# We're not allowed to delete anything anyway.
|
|
return
|
|
|
|
self.notify.info("Configured max usage is: %s MB" % (
|
|
(self.maxDiskUsage + 524288) / 1048576))
|
|
if totalSize <= self.maxDiskUsage:
|
|
# Still within budget; no need to clean up anything.
|
|
return
|
|
|
|
# OK, we're over budget. Now we have to remove old packages.
|
|
usedPackages = []
|
|
for hostData in hosts:
|
|
for packageData in hostData.packages:
|
|
if packageData.package and packageData.package.installed:
|
|
# Don't uninstall any packages we're currently using.
|
|
continue
|
|
|
|
usedPackages.append((packageData.lastUse, packageData))
|
|
|
|
# Sort the packages into oldest-first order.
|
|
usedPackages.sort()
|
|
|
|
# Delete packages until we free up enough space.
|
|
packages = []
|
|
for lastUse, packageData in usedPackages:
|
|
if totalSize <= self.maxDiskUsage:
|
|
break
|
|
totalSize -= packageData.totalSize
|
|
|
|
if packageData.package:
|
|
packages.append(packageData.package)
|
|
else:
|
|
# If it's an unknown package, just delete it directly.
|
|
print "Deleting unknown package %s" % (packageData.pathname)
|
|
self.rmtree(packageData.pathname)
|
|
|
|
packages = self.deletePackages(packages)
|
|
if packages:
|
|
print "Unable to delete %s packages" % (len(packages))
|
|
|
|
return
|
|
|
|
def stop(self):
|
|
""" This method can be called by JavaScript to stop the
|
|
application. """
|
|
|
|
# We defer the actual exit for a few frames, so we don't raise
|
|
# an exception and invalidate the JavaScript call; and also to
|
|
# help protect against race conditions as the application
|
|
# shuts down.
|
|
taskMgr.doMethodLater(0.5, sys.exit, 'exit')
|
|
|
|
def run(self):
|
|
""" This method calls taskMgr.run(), with an optional
|
|
exception handler. This is generally the program's main loop
|
|
when running in a p3d environment (except on unusual platforms
|
|
like the iPhone, which have to hand the main loop off to the
|
|
OS, and don't use this interface). """
|
|
|
|
try:
|
|
taskMgr.run()
|
|
|
|
except SystemExit:
|
|
# Presumably the window has already been shut down here, but shut
|
|
# it down again for good measure.
|
|
if hasattr(__builtin__, "base"):
|
|
base.destroy()
|
|
|
|
self.notify.info("Normal exit.")
|
|
raise
|
|
|
|
except:
|
|
# Some unexpected Python exception; pass it to the
|
|
# optional handler, if it is defined.
|
|
if self.exceptionHandler and not self.interactiveConsole:
|
|
self.exceptionHandler()
|
|
else:
|
|
raise
|
|
|
|
def rmtree(self, filename):
|
|
""" This is like shutil.rmtree(), but it can remove read-only
|
|
files on Windows. It receives a Filename, the root directory
|
|
to delete. """
|
|
if filename.isDirectory():
|
|
for child in filename.scanDirectory():
|
|
self.rmtree(Filename(filename, child))
|
|
if not filename.rmdir():
|
|
print "could not remove directory %s" % (filename)
|
|
else:
|
|
if not filename.unlink():
|
|
print "could not delete %s" % (filename)
|
|
|
|
def setSessionId(self, sessionId):
|
|
""" This message should come in at startup. """
|
|
self.sessionId = sessionId
|
|
self.nextScriptId = self.sessionId * 1000 + 10000
|
|
|
|
def initPackedAppEnvironment(self):
|
|
""" This function sets up the Python environment suitably for
|
|
running a packed app. It should only run once in any given
|
|
session (and it includes logic to ensure this). """
|
|
|
|
if self.packedAppEnvironmentInitialized:
|
|
return
|
|
|
|
self.packedAppEnvironmentInitialized = True
|
|
|
|
vfs = VirtualFileSystem.getGlobalPtr()
|
|
|
|
# Now set up Python to import this stuff.
|
|
VFSImporter.register()
|
|
sys.path.append(self.multifileRoot)
|
|
|
|
# Make sure that $MAIN_DIR is set to the p3d root before we
|
|
# start executing the code in this file.
|
|
ExecutionEnvironment.setEnvironmentVariable("MAIN_DIR", Filename(self.multifileRoot).toOsSpecific())
|
|
|
|
# Put our root directory on the model-path, too.
|
|
getModelPath().appendDirectory(self.multifileRoot)
|
|
|
|
if not self.trueFileIO:
|
|
# Replace the builtin open and file symbols so user code will get
|
|
# our versions by default, which can open and read files out of
|
|
# the multifile.
|
|
__builtin__.file = file.file
|
|
__builtin__.open = file.open
|
|
__builtin__.execfile = file.execfile
|
|
os.listdir = file.listdir
|
|
os.walk = file.walk
|
|
os.path.join = file.join
|
|
os.path.isfile = file.isfile
|
|
os.path.isdir = file.isdir
|
|
os.path.exists = file.exists
|
|
os.path.lexists = file.lexists
|
|
os.path.getmtime = file.getmtime
|
|
os.path.getsize = file.getsize
|
|
sys.modules['glob'] = glob
|
|
|
|
self.checkDiskUsage()
|
|
|
|
def __startIfReady(self):
|
|
""" Called internally to start the application. """
|
|
if self.started:
|
|
return
|
|
|
|
if self.gotWindow and self.gotP3DFilename:
|
|
self.started = True
|
|
|
|
# Now we can ignore future calls to startIfReady().
|
|
self.ignore('AppRunner_startIfReady')
|
|
|
|
# Hang a hook so we know when the window is actually opened.
|
|
self.acceptOnce('window-event', self.__windowEvent)
|
|
|
|
# Look for the startup Python file. This might be a magic
|
|
# filename (like "__main__", or any filename that contains
|
|
# invalid module characters), so we can't just import it
|
|
# directly; instead, we go through the low-level importer.
|
|
|
|
# If there's no p3d_info.xml file, we look for "main".
|
|
moduleName = 'main'
|
|
if self.p3dPackage:
|
|
mainName = self.p3dPackage.Attribute('main_module')
|
|
if mainName:
|
|
moduleName = mainName
|
|
|
|
# Temporarily set this flag while we import the app, so
|
|
# that if the app calls run() within its own main.py, it
|
|
# will properly get ignored by ShowBase.
|
|
self.initialAppImport = True
|
|
|
|
__import__(moduleName)
|
|
main = sys.modules[moduleName]
|
|
if hasattr(main, 'main') and hasattr(main.main, '__call__'):
|
|
main.main(self)
|
|
|
|
# Now clear this flag.
|
|
self.initialAppImport = False
|
|
|
|
if self.interactiveConsole:
|
|
# At this point, we have successfully loaded the app.
|
|
# If the interactive_console flag is enabled, stop the
|
|
# main loop now and give the user a Python prompt.
|
|
taskMgr.stop()
|
|
|
|
def getPandaScriptObject(self):
|
|
""" Called by the browser to query the Panda instance's
|
|
toplevel scripting object, for querying properties in the
|
|
Panda instance. The attributes on this object are mapped to
|
|
document.pluginobject.main within the DOM. """
|
|
|
|
return self.main
|
|
|
|
def setBrowserScriptObject(self, dom):
|
|
""" Called by the browser to supply the browser's toplevel DOM
|
|
object, for controlling the JavaScript and the document in the
|
|
same page with the Panda3D plugin. """
|
|
|
|
self.dom = dom
|
|
|
|
# Now evaluate any deferred expressions.
|
|
for expression in self.deferredEvals:
|
|
self.scriptRequest('eval', self.dom, value = expression,
|
|
needsResponse = False)
|
|
self.deferredEvals = []
|
|
|
|
def setInstanceInfo(self, rootDir, logDirectory, superMirrorUrl,
|
|
verifyContents, main, respectPerPlatform):
|
|
""" Called by the browser to set some global information about
|
|
the instance. """
|
|
|
|
# rootDir is the root Panda3D install directory on the local
|
|
# machine.
|
|
self.rootDir = Filename.fromOsSpecific(rootDir)
|
|
|
|
# logDirectory is the directory name where all log files end
|
|
# up.
|
|
if logDirectory:
|
|
self.logDirectory = Filename.fromOsSpecific(logDirectory)
|
|
else:
|
|
self.logDirectory = Filename(rootDir, 'log')
|
|
|
|
# The "super mirror" URL, generally used only by panda3d.exe.
|
|
self.superMirrorUrl = superMirrorUrl
|
|
|
|
# How anxious should we be about contacting the server for
|
|
# the latest code?
|
|
self.verifyContents = verifyContents
|
|
|
|
# The initial "main" object, if specified.
|
|
if main is not None:
|
|
self.main = main
|
|
|
|
self.respectPerPlatform = respectPerPlatform
|
|
#self.notify.info("respectPerPlatform = %s" % (self.respectPerPlatform))
|
|
|
|
# Now that we have rootDir, we can read the config file.
|
|
self.readConfigXml()
|
|
|
|
|
|
def addPackageInfo(self, name, platform, version, hostUrl, hostDir = None,
|
|
recurse = False):
|
|
""" Called by the browser for each one of the "required"
|
|
packages that were preloaded before starting the application.
|
|
If for some reason the package isn't already downloaded, this
|
|
will download it on the spot. Raises OSError on failure. """
|
|
|
|
host = self.getHost(hostUrl, hostDir = hostDir)
|
|
|
|
if not host.hasContentsFile:
|
|
# Always pre-read these hosts' contents.xml files, even if
|
|
# we have P3DVCForce in effect, since presumably we've
|
|
# already forced them on the plugin side.
|
|
host.readContentsFile()
|
|
|
|
if not host.downloadContentsFile(self.http):
|
|
# Couldn't download? Must have failed to download in the
|
|
# plugin as well. But since we launched, we probably have
|
|
# a copy already local; let's use it.
|
|
message = "Host %s cannot be downloaded, cannot preload %s." % (hostUrl, name)
|
|
if not host.hasContentsFile:
|
|
# This is weird. How did we launch without having
|
|
# this file at all?
|
|
raise OSError, message
|
|
|
|
# Just make it a warning and continue.
|
|
self.notify.warning(message)
|
|
|
|
if name == 'panda3d' and not self.pandaHostUrl:
|
|
# A special case: in case we don't have the PackageHostUrl
|
|
# compiled in, infer it from the first package we
|
|
# installed named "panda3d".
|
|
self.pandaHostUrl = hostUrl
|
|
|
|
if not platform:
|
|
platform = None
|
|
package = host.getPackage(name, version, platform = platform)
|
|
if not package:
|
|
if not recurse:
|
|
# Maybe the contents.xml file isn't current. Re-fetch it.
|
|
if host.redownloadContentsFile(self.http):
|
|
return self.addPackageInfo(name, platform, version, hostUrl, hostDir = hostDir, recurse = True)
|
|
|
|
message = "Couldn't find %s %s on %s" % (name, version, hostUrl)
|
|
raise OSError, message
|
|
|
|
package.checkStatus()
|
|
if not package.downloadDescFile(self.http):
|
|
message = "Couldn't get desc file for %s" % (name)
|
|
raise OSError, message
|
|
|
|
if not package.downloadPackage(self.http):
|
|
message = "Couldn't download %s" % (name)
|
|
raise OSError, message
|
|
|
|
if not package.installPackage(self):
|
|
message = "Couldn't install %s" % (name)
|
|
raise OSError, message
|
|
|
|
if package.guiApp:
|
|
self.guiApp = True
|
|
initAppForGui()
|
|
|
|
def setP3DFilename(self, p3dFilename, tokens, argv, instanceId,
|
|
interactiveConsole, p3dOffset = 0, p3dUrl = None):
|
|
""" Called by the browser to specify the p3d file that
|
|
contains the application itself, along with the web tokens
|
|
and/or command-line arguments. Once this method has been
|
|
called, the application is effectively started. """
|
|
|
|
# One day we will have support for multiple instances within a
|
|
# Python session. Against that day, we save the instance ID
|
|
# for this instance.
|
|
self.instanceId = instanceId
|
|
|
|
self.tokens = tokens
|
|
self.argv = argv
|
|
|
|
# We build up a token dictionary with care, so that if a given
|
|
# token appears twice in the token list, we record only the
|
|
# first value, not the second or later. This is consistent
|
|
# with the internal behavior of the core API.
|
|
self.tokenDict = {}
|
|
for token, keyword in tokens:
|
|
self.tokenDict.setdefault(token, keyword)
|
|
|
|
# Also store the arguments on sys, for applications that
|
|
# aren't instance-ready.
|
|
sys.argv = argv
|
|
|
|
# That means we now know the altHost in effect.
|
|
self.altHost = self.tokenDict.get('alt_host', None)
|
|
|
|
# Tell the browser that Python is up and running, and ready to
|
|
# respond to queries.
|
|
self.notifyRequest('onpythonload')
|
|
|
|
# Now go load the applet.
|
|
fname = Filename.fromOsSpecific(p3dFilename)
|
|
vfs = VirtualFileSystem.getGlobalPtr()
|
|
|
|
if not vfs.exists(fname):
|
|
raise ArgumentError, "No such file: %s" % (p3dFilename)
|
|
|
|
fname.makeAbsolute()
|
|
fname.setBinary()
|
|
mf = Multifile()
|
|
if p3dOffset == 0:
|
|
if not mf.openRead(fname):
|
|
raise ArgumentError, "Not a Panda3D application: %s" % (p3dFilename)
|
|
else:
|
|
if not mf.openRead(fname, p3dOffset):
|
|
raise ArgumentError, "Not a Panda3D application: %s at offset: %s" % (p3dFilename, p3dOffset)
|
|
|
|
# Now load the p3dInfo file.
|
|
self.p3dInfo = None
|
|
self.p3dPackage = None
|
|
self.p3dConfig = None
|
|
self.allowPythonDev = False
|
|
|
|
i = mf.findSubfile('p3d_info.xml')
|
|
if i >= 0 and hasattr(PandaModules, 'readXmlStream'):
|
|
stream = mf.openReadSubfile(i)
|
|
self.p3dInfo = PandaModules.readXmlStream(stream)
|
|
mf.closeReadSubfile(stream)
|
|
if self.p3dInfo:
|
|
self.p3dPackage = self.p3dInfo.FirstChildElement('package')
|
|
if self.p3dPackage:
|
|
self.p3dConfig = self.p3dPackage.FirstChildElement('config')
|
|
|
|
xhost = self.p3dPackage.FirstChildElement('host')
|
|
while xhost:
|
|
self.__readHostXml(xhost)
|
|
xhost = xhost.NextSiblingElement('host')
|
|
|
|
if self.p3dConfig:
|
|
allowPythonDev = self.p3dConfig.Attribute('allow_python_dev')
|
|
if allowPythonDev:
|
|
self.allowPythonDev = int(allowPythonDev)
|
|
guiApp = self.p3dConfig.Attribute('gui_app')
|
|
if guiApp:
|
|
self.guiApp = int(guiApp)
|
|
|
|
trueFileIO = self.p3dConfig.Attribute('true_file_io')
|
|
if trueFileIO:
|
|
self.trueFileIO = int(trueFileIO)
|
|
|
|
# The interactiveConsole flag can only be set true if the
|
|
# application has allow_python_dev set.
|
|
if not self.allowPythonDev and interactiveConsole:
|
|
raise StandardError, "Impossible, interactive_console set without allow_python_dev."
|
|
self.interactiveConsole = interactiveConsole
|
|
|
|
if self.allowPythonDev:
|
|
# Set the fps text to remind the user that
|
|
# allow_python_dev is enabled.
|
|
ConfigVariableString('frame-rate-meter-text-pattern').setValue('allow_python_dev %0.1f fps')
|
|
|
|
if self.guiApp:
|
|
initAppForGui()
|
|
|
|
self.initPackedAppEnvironment()
|
|
|
|
# Mount the Multifile under self.multifileRoot.
|
|
vfs.mount(mf, self.multifileRoot, vfs.MFReadOnly)
|
|
self.p3dMultifile = mf
|
|
VFSImporter.reloadSharedPackages()
|
|
|
|
self.loadMultifilePrcFiles(mf, self.multifileRoot)
|
|
self.gotP3DFilename = True
|
|
self.p3dFilename = fname
|
|
if p3dUrl:
|
|
# The url from which the p3d file was downloaded is
|
|
# provided if available. It is only for documentation
|
|
# purposes; the actual p3d file has already been
|
|
# downloaded to p3dFilename.
|
|
self.p3dUrl = PandaModules.URLSpec(p3dUrl)
|
|
|
|
# Send this call to the main thread; don't call it directly.
|
|
messenger.send('AppRunner_startIfReady', taskChain = 'default')
|
|
|
|
def __readHostXml(self, xhost):
|
|
""" Reads the data in the indicated <host> entry. """
|
|
|
|
url = xhost.Attribute('url')
|
|
host = self.getHost(url)
|
|
host.readHostXml(xhost)
|
|
|
|
# Scan for a matching <alt_host>. If found, it means we
|
|
# should use the alternate URL instead of the original URL.
|
|
if self.altHost:
|
|
xalthost = xhost.FirstChildElement('alt_host')
|
|
while xalthost:
|
|
keyword = xalthost.Attribute('keyword')
|
|
if keyword == self.altHost:
|
|
origUrl = xhost.Attribute('url')
|
|
newUrl = xalthost.Attribute('url')
|
|
self.altHostMap[origUrl] = newUrl
|
|
break
|
|
|
|
xalthost = xalthost.NextSiblingElement('alt_host')
|
|
|
|
def loadMultifilePrcFiles(self, mf, root):
|
|
""" Loads any prc files in the root of the indicated
|
|
Multifile, which is presumed 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.
|
|
cpMgr = ConfigPageManager.getGlobalPtr()
|
|
for f in mf.getSubfileNames():
|
|
fn = Filename(f)
|
|
if fn.getDirname() == '' and fn.getExtension() == 'prc':
|
|
pathname = '%s/%s' % (root, f)
|
|
|
|
alreadyLoaded = False
|
|
for cpi in range(cpMgr.getNumImplicitPages()):
|
|
if cpMgr.getImplicitPage(cpi).getName() == pathname:
|
|
# No need to load this file twice.
|
|
alreadyLoaded = True
|
|
break
|
|
|
|
if not alreadyLoaded:
|
|
data = file.open(Filename(pathname), 'r').read()
|
|
cp = loadPrcFileData(pathname, data)
|
|
# Set it to sort value 20, behind the implicit pages.
|
|
cp.setSort(20)
|
|
|
|
|
|
def __clearWindowProperties(self):
|
|
""" Clears the windowPrc file that was created in a previous
|
|
call to setupWindow(), if any. """
|
|
|
|
if self.windowPrc:
|
|
unloadPrcFile(self.windowPrc)
|
|
self.windowPrc = None
|
|
WindowProperties.clearDefault()
|
|
|
|
# However, we keep the self.windowProperties object around, in
|
|
# case an application wants to return the window to the
|
|
# browser frame.
|
|
|
|
def setupWindow(self, windowType, x, y, width, height,
|
|
parent):
|
|
""" Applies the indicated window parameters to the prc
|
|
settings, for future windows; or applies them directly to the
|
|
main window if the window has already been opened. This is
|
|
called by the browser. """
|
|
|
|
if self.started and base.win:
|
|
# If we've already got a window, this must be a
|
|
# resize/reposition request.
|
|
wp = WindowProperties()
|
|
if x or y or windowType == 'embedded':
|
|
wp.setOrigin(x, y)
|
|
if width or height:
|
|
wp.setSize(width, height)
|
|
if windowType == 'embedded':
|
|
wp.setParentWindow(parent)
|
|
wp.setFullscreen(False)
|
|
base.win.requestProperties(wp)
|
|
self.windowProperties = wp
|
|
return
|
|
|
|
# If we haven't got a window already, start 'er up. Apply the
|
|
# requested setting to the prc file, and to the default
|
|
# WindowProperties structure.
|
|
|
|
self.__clearWindowProperties()
|
|
|
|
if windowType == 'hidden':
|
|
data = 'window-type none\n'
|
|
else:
|
|
data = 'window-type onscreen\n'
|
|
|
|
wp = WindowProperties.getDefault()
|
|
|
|
wp.clearParentWindow()
|
|
wp.clearOrigin()
|
|
wp.clearSize()
|
|
|
|
wp.setFullscreen(False)
|
|
if windowType == 'fullscreen':
|
|
wp.setFullscreen(True)
|
|
|
|
if windowType == 'embedded':
|
|
wp.setParentWindow(parent)
|
|
|
|
if x or y or windowType == 'embedded':
|
|
wp.setOrigin(x, y)
|
|
|
|
if width or height:
|
|
wp.setSize(width, height)
|
|
|
|
self.windowProperties = wp
|
|
self.windowPrc = loadPrcFileData("setupWindow", data)
|
|
WindowProperties.setDefault(wp)
|
|
|
|
self.gotWindow = True
|
|
|
|
# Send this call to the main thread; don't call it directly.
|
|
messenger.send('AppRunner_startIfReady', taskChain = 'default')
|
|
|
|
def setRequestFunc(self, func):
|
|
""" This method is called by the browser at startup to supply a
|
|
function that can be used to deliver requests upstream, to the
|
|
core API, and thereby to the browser. """
|
|
self.requestFunc = func
|
|
|
|
def sendRequest(self, request, *args):
|
|
""" Delivers a request to the browser via self.requestFunc.
|
|
This low-level function is not intended to be called directly
|
|
by user code. """
|
|
|
|
assert self.requestFunc
|
|
return self.requestFunc(self.instanceId, request, args)
|
|
|
|
def __windowEvent(self, win):
|
|
""" This method is called when we get a window event. We
|
|
listen for this to detect when the window has been
|
|
successfully opened. """
|
|
|
|
if not self.windowOpened:
|
|
self.windowOpened = True
|
|
|
|
# Now that the window is open, we don't need to keep those
|
|
# prc settings around any more.
|
|
self.__clearWindowProperties()
|
|
|
|
# Inform the plugin and browser.
|
|
self.notifyRequest('onwindowopen')
|
|
|
|
def notifyRequest(self, message):
|
|
""" Delivers a notify request to the browser. This is a "this
|
|
happened" type notification; it also triggers some JavaScript
|
|
code execution, if indicated in the HTML tags, and may also
|
|
trigger some internal automatic actions. (For instance, the
|
|
plugin takes down the splash window when it sees the
|
|
onwindowopen notification. """
|
|
|
|
self.sendRequest('notify', message.lower())
|
|
|
|
def evalScript(self, expression, needsResponse = False):
|
|
""" Evaluates an arbitrary JavaScript expression in the global
|
|
DOM space. This may be deferred if necessary if needsResponse
|
|
is False and self.dom has not yet been assigned. If
|
|
needsResponse is true, this waits for the value and returns
|
|
it, which means it cannot be deferred. """
|
|
|
|
if not self.dom:
|
|
# Defer the expression.
|
|
assert not needsResponse
|
|
self.deferredEvals.append(expression)
|
|
else:
|
|
# Evaluate it now.
|
|
return self.scriptRequest('eval', self.dom, value = expression,
|
|
needsResponse = needsResponse)
|
|
|
|
def scriptRequest(self, operation, object, propertyName = '',
|
|
value = None, needsResponse = True):
|
|
""" Issues a new script request to the browser. This queries
|
|
or modifies one of the browser's DOM properties. This is a
|
|
low-level method that user code should not call directly;
|
|
instead, just operate on the Python wrapper objects that
|
|
shadow the DOM objects, beginning with appRunner.dom.
|
|
|
|
operation may be one of [ 'get_property', 'set_property',
|
|
'call', 'evaluate' ].
|
|
|
|
object is the browser object to manipulate, or the scope in
|
|
which to evaluate the expression.
|
|
|
|
propertyName is the name of the property to manipulate, if
|
|
relevant (set to None for the default method name).
|
|
|
|
value is the new value to assign to the property for
|
|
set_property, or the parameter list for call, or the string
|
|
expression for evaluate.
|
|
|
|
If needsResponse is true, this method will block until the
|
|
return value is received from the browser, and then it returns
|
|
that value. Otherwise, it returns None immediately, without
|
|
waiting for the browser to process the request.
|
|
"""
|
|
uniqueId = self.nextScriptId
|
|
self.nextScriptId = (self.nextScriptId + 1) % 0xffffffff
|
|
self.sendRequest('script', operation, object,
|
|
propertyName, value, needsResponse, uniqueId)
|
|
|
|
if needsResponse:
|
|
# Now wait for the response to come in.
|
|
result = self.sendRequest('wait_script_response', uniqueId)
|
|
return result
|
|
|
|
def dropObject(self, objectId):
|
|
""" Inform the parent process that we no longer have an
|
|
interest in the P3D_object corresponding to the indicated
|
|
objectId. Not intended to be called by user code. """
|
|
|
|
self.sendRequest('drop_p3dobj', objectId)
|
|
|
|
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
|
|
the start of your application to enable it.
|
|
|
|
It places the current working directory under /mf, as if it were
|
|
mounted from a packed multifile. It doesn't convert egg files to
|
|
bam files, of course; and there are other minor differences from
|
|
running in an actual packaged environment. But it can be a useful
|
|
first-look sanity check. """
|
|
|
|
if AppRunnerGlobal.appRunner:
|
|
print "Already have AppRunner, not creating a new one."
|
|
return AppRunnerGlobal.appRunner
|
|
|
|
appRunner = AppRunner()
|
|
appRunner.dummy = True
|
|
AppRunnerGlobal.appRunner = appRunner
|
|
|
|
platform = PandaSystem.getPlatform()
|
|
version = PandaSystem.getPackageVersionString()
|
|
hostUrl = PandaSystem.getPackageHostUrl()
|
|
|
|
if platform.startswith('win'):
|
|
rootDir = Filename(Filename.getUserAppdataDirectory(), 'Panda3D')
|
|
elif platform.startswith('osx'):
|
|
rootDir = Filename(Filename.getHomeDirectory(), 'Library/Caches/Panda3D')
|
|
else:
|
|
rootDir = Filename(Filename.getHomeDirectory(), '.panda3d')
|
|
|
|
appRunner.rootDir = rootDir
|
|
appRunner.logDirectory = Filename(rootDir, 'log')
|
|
|
|
# Of course we will have the panda3d application loaded.
|
|
appRunner.addPackageInfo('panda3d', platform, version, hostUrl)
|
|
|
|
appRunner.tokens = tokens
|
|
appRunner.tokenDict = dict(tokens)
|
|
if argv is None:
|
|
argv = sys.argv
|
|
appRunner.argv = argv
|
|
appRunner.altHost = appRunner.tokenDict.get('alt_host', None)
|
|
|
|
appRunner.p3dInfo = None
|
|
appRunner.p3dPackage = None
|
|
|
|
# Mount the current directory under the multifileRoot, as if it
|
|
# were coming from a multifile.
|
|
cwd = ExecutionEnvironment.getCwd()
|
|
vfs = VirtualFileSystem.getGlobalPtr()
|
|
vfs.mount(cwd, appRunner.multifileRoot, vfs.MFReadOnly)
|
|
|
|
appRunner.initPackedAppEnvironment()
|
|
|
|
return appRunner
|
|
|