automatic package management
This commit is contained in:
parent
e68e597b6a
commit
b4927284b5
|
|
@ -12,6 +12,7 @@ runp3d.py for a command-line tool to invoke this module.
|
|||
import sys
|
||||
import os
|
||||
import types
|
||||
import shutil
|
||||
import __builtin__
|
||||
|
||||
if 'VFSImporter' in sys.modules:
|
||||
|
|
@ -33,7 +34,7 @@ else:
|
|||
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, initAppForGui
|
||||
from pandac.PandaModules import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, Thread, WindowProperties, ExecutionEnvironment, PandaSystem, Notify, StreamWriter, ConfigVariableString, initAppForGui, TiXmlDocument
|
||||
from pandac import PandaModules
|
||||
from direct.stdpy import file, glob
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
|
|
@ -41,6 +42,9 @@ 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
|
||||
|
|
@ -66,6 +70,11 @@ class AppRunner(DirectObject):
|
|||
development purposes. """
|
||||
|
||||
notify = directNotify.newCategory("AppRunner")
|
||||
|
||||
ConfigBasename = 'config.xml'
|
||||
|
||||
# Default values for parameters that are absent from the config file:
|
||||
maxDiskUsage = 1073741824 # 1 GB
|
||||
|
||||
def __init__(self):
|
||||
DirectObject.__init__(self)
|
||||
|
|
@ -344,6 +353,63 @@ class AppRunner(DirectObject):
|
|||
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.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))
|
||||
shutil.rmtree(host.hostDir.toOsSpecific(), True)
|
||||
|
||||
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,
|
||||
|
|
@ -392,6 +458,114 @@ class AppRunner(DirectObject):
|
|||
|
||||
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():
|
||||
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
|
||||
|
||||
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 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))
|
||||
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)
|
||||
shutil.rmtree(packageData.pathname.toOsSpecific(), True)
|
||||
|
||||
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. """
|
||||
|
|
@ -473,6 +647,8 @@ class AppRunner(DirectObject):
|
|||
os.path.getsize = file.getsize
|
||||
sys.modules['glob'] = glob
|
||||
|
||||
self.checkDiskUsage()
|
||||
|
||||
def __startIfReady(self):
|
||||
""" Called internally to start the application. """
|
||||
if self.started:
|
||||
|
|
@ -557,6 +733,10 @@ class AppRunner(DirectObject):
|
|||
# The "super mirror" URL, generally used only by panda3d.exe.
|
||||
self.superMirrorUrl = superMirrorUrl
|
||||
|
||||
# Now that we have rootDir, we can read the config file.
|
||||
self.readConfigXml()
|
||||
|
||||
|
||||
def addPackageInfo(self, name, platform, version, hostUrl, hostDir = None):
|
||||
""" Called by the browser for each one of the "required"
|
||||
packages that were preloaded before starting the application.
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ class FileSpec:
|
|||
return False
|
||||
|
||||
if notify:
|
||||
notify.debug("hash check ok: %s" % (pathname))
|
||||
notify.info("hash check ok: %s" % (pathname))
|
||||
|
||||
# The hash is OK after all. Change the file's timestamp back
|
||||
# to what we expect it to be, so we can quick-verify it
|
||||
|
|
@ -164,7 +164,7 @@ class FileSpec:
|
|||
return False
|
||||
|
||||
if notify:
|
||||
notify.debug("hash check ok: %s" % (pathname))
|
||||
notify.info("hash check ok: %s" % (pathname))
|
||||
|
||||
# The hash is OK. If the timestamp is wrong, change it back
|
||||
# to what we expect it to be, so we can quick-verify it
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from direct.p3d.PackageInfo import PackageInfo
|
|||
from direct.p3d.FileSpec import FileSpec
|
||||
from direct.directnotify.DirectNotifyGlobal import directNotify
|
||||
import time
|
||||
import shutil
|
||||
|
||||
class HostInfo:
|
||||
""" This class represents a particular download host serving up
|
||||
|
|
@ -35,8 +36,8 @@ class HostInfo:
|
|||
the default is perPlatform = True. """
|
||||
|
||||
assert appRunner or rootDir or hostDir
|
||||
|
||||
self.hostUrl = hostUrl
|
||||
|
||||
self.__setHostUrl(hostUrl)
|
||||
self.appRunner = appRunner
|
||||
self.rootDir = rootDir
|
||||
if rootDir is None and appRunner:
|
||||
|
|
@ -48,18 +49,6 @@ class HostInfo:
|
|||
if perPlatform is None:
|
||||
self.perPlatform = asMirror
|
||||
|
||||
# hostUrlPrefix is the host URL, but it is guaranteed to end
|
||||
# with a slash.
|
||||
self.hostUrlPrefix = hostUrl
|
||||
if self.hostUrlPrefix[-1] != '/':
|
||||
self.hostUrlPrefix += '/'
|
||||
|
||||
# downloadUrlPrefix is the URL prefix that should be used for
|
||||
# everything other than the contents.xml file. It might be
|
||||
# the same as hostUrlPrefix, but in the case of an
|
||||
# https-protected hostUrl, it will be the cleartext channel.
|
||||
self.downloadUrlPrefix = self.hostUrlPrefix
|
||||
|
||||
# Initially false, this is set true when the contents file is
|
||||
# successfully read.
|
||||
self.hasContentsFile = False
|
||||
|
|
@ -82,6 +71,27 @@ class HostInfo:
|
|||
# will be filled in when the contents file is read.
|
||||
self.packages = {}
|
||||
|
||||
def __setHostUrl(self, hostUrl):
|
||||
""" Assigns self.hostUrl, and related values. """
|
||||
self.hostUrl = hostUrl
|
||||
|
||||
if self.hostUrl is None:
|
||||
# A special case: the URL will be set later.
|
||||
self.hostUrlPrefix = None
|
||||
self.downloadUrlPrefix = None
|
||||
else:
|
||||
# hostUrlPrefix is the host URL, but it is guaranteed to end
|
||||
# with a slash.
|
||||
self.hostUrlPrefix = hostUrl
|
||||
if self.hostUrlPrefix[-1] != '/':
|
||||
self.hostUrlPrefix += '/'
|
||||
|
||||
# downloadUrlPrefix is the URL prefix that should be used for
|
||||
# everything other than the contents.xml file. It might be
|
||||
# the same as hostUrlPrefix, but in the case of an
|
||||
# https-protected hostUrl, it will be the cleartext channel.
|
||||
self.downloadUrlPrefix = self.hostUrlPrefix
|
||||
|
||||
def downloadContentsFile(self, http, redownload = False):
|
||||
""" Downloads the contents.xml file for this particular host,
|
||||
synchronously, and then reads it. Returns true on success,
|
||||
|
|
@ -213,10 +223,14 @@ class HostInfo:
|
|||
return False
|
||||
|
||||
# Look for our own entry in the hosts table.
|
||||
self.__findHostXml(xcontents)
|
||||
if self.hostUrl:
|
||||
self.__findHostXml(xcontents)
|
||||
else:
|
||||
assert self.hostDir
|
||||
self.__findHostXmlForHostDir(xcontents)
|
||||
|
||||
if not self.hostDir:
|
||||
self.__determineHostDir(None)
|
||||
self.hostDir = self.__determineHostDir(None, self.hostUrl)
|
||||
|
||||
# Get the list of packages available for download and/or import.
|
||||
xpackage = xcontents.FirstChildElement('package')
|
||||
|
|
@ -275,6 +289,36 @@ class HostInfo:
|
|||
|
||||
xhost = xhost.NextSiblingElement('host')
|
||||
|
||||
def __findHostXmlForHostDir(self, xcontents):
|
||||
""" Looks for the <host> or <alt_host> entry in the
|
||||
contents.xml that corresponds to the host dir that we read the
|
||||
contents.xml from. This is used when reading a contents.xml
|
||||
file found on disk, as opposed to downloading it from a
|
||||
site. """
|
||||
|
||||
xhost = xcontents.FirstChildElement('host')
|
||||
while xhost:
|
||||
url = xhost.Attribute('url')
|
||||
hostDirBasename = xhost.Attribute('host_dir')
|
||||
hostDir = self.__determineHostDir(hostDirBasename, url)
|
||||
if hostDir == self.hostDir:
|
||||
self.__setHostUrl(url)
|
||||
self.readHostXml(xhost)
|
||||
return
|
||||
|
||||
xalthost = xhost.FirstChildElement('alt_host')
|
||||
while xalthost:
|
||||
url = xalthost.Attribute('url')
|
||||
hostDirBasename = xalthost.Attribute('host_dir')
|
||||
hostDir = self.__determineHostDir(hostDirBasename, url)
|
||||
if hostDir == self.hostDir:
|
||||
self.__setHostUrl(url)
|
||||
self.readHostXml(xalthost)
|
||||
return
|
||||
xalthost = xalthost.NextSiblingElement('alt_host')
|
||||
|
||||
xhost = xhost.NextSiblingElement('host')
|
||||
|
||||
def readHostXml(self, xhost):
|
||||
""" Reads a <host> or <alt_host> entry and applies the data to
|
||||
this object. """
|
||||
|
|
@ -284,7 +328,8 @@ class HostInfo:
|
|||
self.descriptiveName = descriptiveName
|
||||
|
||||
hostDirBasename = xhost.Attribute('host_dir')
|
||||
self.__determineHostDir(hostDirBasename)
|
||||
if not self.hostDir:
|
||||
self.hostDir = self.__determineHostDir(hostDirBasename, self.hostUrl)
|
||||
|
||||
# Get the "download" URL, which is the source from which we
|
||||
# download everything other than the contents.xml file.
|
||||
|
|
@ -381,29 +426,85 @@ class HostInfo:
|
|||
|
||||
return packages
|
||||
|
||||
def __determineHostDir(self, hostDirBasename):
|
||||
def getAllPackages(self):
|
||||
""" Returns a list of all available packages provided by this
|
||||
host. """
|
||||
|
||||
result = []
|
||||
|
||||
items = self.packages.items()
|
||||
items.sort()
|
||||
for key, platforms in items:
|
||||
if self.perPlatform:
|
||||
# If we maintain a different answer per platform,
|
||||
# return all of them.
|
||||
pitems = platforms.items()
|
||||
pitems.sort()
|
||||
for pkey, package in pitems:
|
||||
result.append(package)
|
||||
else:
|
||||
# If we maintain a a host for the current platform
|
||||
# only (e.g. a client copy), then return only the
|
||||
# current platform, or no particular platform.
|
||||
package = platforms.get(PandaSystem.getPlatform(), None)
|
||||
if not package:
|
||||
package = platforms.get(None, None)
|
||||
|
||||
if package:
|
||||
result.append(package)
|
||||
|
||||
return result
|
||||
|
||||
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 getPackage(). Returns
|
||||
the list of packages that were NOT found. """
|
||||
|
||||
packages = packages[:]
|
||||
|
||||
for key, platforms in self.packages.items():
|
||||
for platform, package in platforms.items():
|
||||
if package in packages:
|
||||
self.__deletePackageFiles(package)
|
||||
del platforms[platform]
|
||||
packages.remove(package)
|
||||
|
||||
if not platforms:
|
||||
# If we've removed all the platforms for a given
|
||||
# package, remove the key from the toplevel map.
|
||||
del self.packages[key]
|
||||
|
||||
return packages
|
||||
|
||||
def __deletePackageFiles(self, package):
|
||||
""" Called by deletePackage(), this actually removes the files
|
||||
for the indicated package. """
|
||||
|
||||
self.notify.info("Deleting package %s: %s" % (package.packageName, package.getPackageDir()))
|
||||
shutil.rmtree(package.getPackageDir().toOsSpecific(), True)
|
||||
|
||||
def __determineHostDir(self, hostDirBasename, hostUrl):
|
||||
""" Hashes the host URL into a (mostly) unique directory
|
||||
string, which will be the root of the host's install tree.
|
||||
Stores the resulting path, as a Filename, in self.hostDir.
|
||||
Returns the resulting path, as a Filename.
|
||||
|
||||
This code is duplicated in C++, in
|
||||
P3DHost::determine_host_dir(). """
|
||||
|
||||
if self.hostDir:
|
||||
return
|
||||
|
||||
if hostDirBasename:
|
||||
# If the contents.xml specified a host_dir parameter, use
|
||||
# it.
|
||||
self.hostDir = self.rootDir + '/hosts'
|
||||
hostDir = self.rootDir.cStr() + '/hosts'
|
||||
for component in hostDirBasename.split('/'):
|
||||
if component:
|
||||
if component[0] == '.':
|
||||
# Forbid ".foo" or "..".
|
||||
component = 'x' + component
|
||||
self.hostDir += '/'
|
||||
self.hostDir += component
|
||||
return
|
||||
hostDir += '/'
|
||||
hostDir += component
|
||||
return Filename(hostDir)
|
||||
|
||||
hostDir = 'hosts/'
|
||||
|
||||
|
|
@ -414,22 +515,22 @@ class HostInfo:
|
|||
# We could use URLSpec, but we do it by hand instead, to make
|
||||
# it more likely that our hash code will exactly match the
|
||||
# similar logic in P3DHost.
|
||||
p = self.hostUrl.find('://')
|
||||
p = hostUrl.find('://')
|
||||
if p != -1:
|
||||
start = p + 3
|
||||
end = self.hostUrl.find('/', start)
|
||||
end = hostUrl.find('/', start)
|
||||
# Now start .. end is something like "username@host:port".
|
||||
|
||||
at = self.hostUrl.find('@', start)
|
||||
at = hostUrl.find('@', start)
|
||||
if at != -1 and at < end:
|
||||
start = at + 1
|
||||
|
||||
colon = self.hostUrl.find(':', start)
|
||||
colon = hostUrl.find(':', start)
|
||||
if colon != -1 and colon < end:
|
||||
end = colon
|
||||
|
||||
# Now start .. end is just the hostname.
|
||||
hostname = self.hostUrl[start : end]
|
||||
hostname = hostUrl[start : end]
|
||||
|
||||
# Now build a hash string of the whole URL. We'll use MD5 to
|
||||
# get a pretty good hash, with a minimum chance of collision.
|
||||
|
|
@ -450,7 +551,8 @@ class HostInfo:
|
|||
keepHash = keepHash / 2;
|
||||
|
||||
md = HashVal()
|
||||
md.hashString(self.hostUrl)
|
||||
md.hashString(hostUrl)
|
||||
hostDir += md.asHex()[:keepHash * 2]
|
||||
|
||||
self.hostDir = Filename(self.rootDir, hostDir)
|
||||
hostDir = Filename(self.rootDir, hostDir)
|
||||
return hostDir
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
from pandac.PandaModules import URLSpec
|
||||
|
||||
class InstalledHostData:
|
||||
""" A list of instances of this class is returned by
|
||||
AppRunner.scanInstalledPackages(). Each of these corresponds to a
|
||||
particular host that has provided packages that have been
|
||||
installed on the local client. """
|
||||
|
||||
def __init__(self, host, dirnode):
|
||||
self.host = host
|
||||
self.pathname = dirnode.pathname
|
||||
self.totalSize = dirnode.getTotalSize()
|
||||
self.packages = []
|
||||
|
||||
if self.host:
|
||||
self.hostUrl = self.host.hostUrl
|
||||
self.descriptiveName = self.host.descriptiveName
|
||||
if not self.descriptiveName:
|
||||
self.descriptiveName = URLSpec(self.hostUrl).getServer()
|
||||
else:
|
||||
self.hostUrl = 'unknown'
|
||||
self.descriptiveName = 'unknown'
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
class InstalledPackageData:
|
||||
""" A list of instances of this class is maintained by
|
||||
InstalledHostData (which is in turn returned by
|
||||
AppRunner.scanInstalledPackages()). Each of these corresponds to
|
||||
a particular package that has been installed on the local
|
||||
client. """
|
||||
|
||||
def __init__(self, package, dirnode):
|
||||
self.package = package
|
||||
self.pathname = dirnode.pathname
|
||||
self.totalSize = dirnode.getTotalSize()
|
||||
self.lastUse = None
|
||||
|
||||
if self.package:
|
||||
self.displayName = self.package.getFormattedName()
|
||||
xusage = self.package.getUsage()
|
||||
|
||||
if xusage:
|
||||
lastUse = xusage.Attribute('last_use')
|
||||
try:
|
||||
lastUse = int(lastUse or '')
|
||||
except ValueError:
|
||||
lastUse = None
|
||||
self.lastUse = lastUse
|
||||
|
||||
else:
|
||||
self.displayName = dirnode.pathname.getBasename()
|
||||
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from pandac.PandaModules import Filename, URLSpec, DocumentSpec, Ramfile, Multifile, Decompressor, EUOk, EUSuccess, VirtualFileSystem, Thread, getModelPath, ExecutionEnvironment, PStatCollector
|
||||
from pandac.PandaModules import Filename, URLSpec, DocumentSpec, Ramfile, Multifile, Decompressor, EUOk, EUSuccess, VirtualFileSystem, Thread, getModelPath, ExecutionEnvironment, PStatCollector, TiXmlDocument, TiXmlDeclaration, TiXmlElement
|
||||
from pandac import PandaModules
|
||||
from direct.p3d.FileSpec import FileSpec
|
||||
from direct.p3d.ScanDirectoryNode import ScanDirectoryNode
|
||||
from direct.showbase import VFSImporter
|
||||
from direct.directnotify.DirectNotifyGlobal import directNotify
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
|
|
@ -8,6 +9,7 @@ import os
|
|||
import sys
|
||||
import random
|
||||
import time
|
||||
import copy
|
||||
|
||||
class PackageInfo:
|
||||
|
||||
|
|
@ -32,6 +34,8 @@ class PackageInfo:
|
|||
restartDownload = 3
|
||||
stepContinue = 4
|
||||
|
||||
UsageBasename = 'usage.xml'
|
||||
|
||||
class InstallStep:
|
||||
""" This class is one step of the installPlan list; it
|
||||
represents a single atomic piece of the installation step, and
|
||||
|
|
@ -117,6 +121,11 @@ class PackageInfo:
|
|||
# meaning it's been added to the paths and all.
|
||||
self.installed = False
|
||||
|
||||
# This is set true when the package has been updated in this
|
||||
# session, but not yet written to usage.xml.
|
||||
self.updated = False
|
||||
self.diskSpace = None
|
||||
|
||||
def getPackageDir(self):
|
||||
""" Returns the directory in which this package is installed.
|
||||
This may not be known until the host's contents.xml file has
|
||||
|
|
@ -493,6 +502,7 @@ class PackageInfo:
|
|||
contents = self.__scanDirectoryRecursively(self.getPackageDir())
|
||||
self.__removeFileFromList(contents, self.descFileBasename)
|
||||
self.__removeFileFromList(contents, self.compressedArchive.filename)
|
||||
self.__removeFileFromList(contents, self.UsageBasename)
|
||||
if not self.asMirror:
|
||||
self.__removeFileFromList(contents, self.uncompressedArchive.filename)
|
||||
for file in self.extracts:
|
||||
|
|
@ -507,6 +517,7 @@ class PackageInfo:
|
|||
self.notify.info("Removing %s" % (filename))
|
||||
pathname = Filename(self.getPackageDir(), filename)
|
||||
pathname.unlink()
|
||||
self.updated = True
|
||||
|
||||
if self.asMirror:
|
||||
return self.compressedArchive.quickVerify(self.getPackageDir(), notify = self.notify)
|
||||
|
|
@ -681,6 +692,8 @@ class PackageInfo:
|
|||
packageDir. Yields one of stepComplete, stepFailed,
|
||||
restartDownload, or stepContinue. """
|
||||
|
||||
self.updated = True
|
||||
|
||||
if not urlbase:
|
||||
urlbase = self.descFileDirname + '/' + fileSpec.filename
|
||||
|
||||
|
|
@ -817,6 +830,8 @@ class PackageInfo:
|
|||
operation. Yields one of stepComplete, stepFailed,
|
||||
restartDownload, or stepContinue. """
|
||||
|
||||
self.updated = True
|
||||
|
||||
origPathname = Filename(self.getPackageDir(), self.uncompressedArchive.filename)
|
||||
patchPathname = Filename(self.getPackageDir(), patchfile.file.filename)
|
||||
result = Filename.temporary('', 'patch_')
|
||||
|
|
@ -857,6 +872,8 @@ class PackageInfo:
|
|||
archive. Yields one of stepComplete, stepFailed,
|
||||
restartDownload, or stepContinue. """
|
||||
|
||||
self.updated = True
|
||||
|
||||
sourcePathname = Filename(self.getPackageDir(), self.compressedArchive.filename)
|
||||
targetPathname = Filename(self.getPackageDir(), self.uncompressedArchive.filename)
|
||||
targetPathname.unlink()
|
||||
|
|
@ -905,6 +922,8 @@ class PackageInfo:
|
|||
self.hasPackage = True
|
||||
yield self.stepComplete; return
|
||||
|
||||
self.updated = True
|
||||
|
||||
mfPathname = Filename(self.getPackageDir(), self.uncompressedArchive.filename)
|
||||
self.notify.info("Unpacking %s" % (mfPathname))
|
||||
mf = Multifile()
|
||||
|
|
@ -1026,4 +1045,94 @@ class PackageInfo:
|
|||
self.installed = True
|
||||
appRunner.installedPackages.append(self)
|
||||
|
||||
self.markUsed()
|
||||
|
||||
return True
|
||||
|
||||
def __measureDiskSpace(self):
|
||||
""" Returns the amount of space used by this package, in
|
||||
bytes, as determined by examining the actual contents of the
|
||||
package directory and its subdirectories. """
|
||||
|
||||
thisDir = ScanDirectoryNode(self.getPackageDir(), ignoreUsageXml = True)
|
||||
diskSpace = thisDir.getTotalSize()
|
||||
self.notify.info("Package %s uses %s MB" % (
|
||||
self.packageName, (diskSpace + 524288) / 1048576))
|
||||
return diskSpace
|
||||
|
||||
def markUsed(self):
|
||||
""" Marks the package as having been used. This is normally
|
||||
called automatically by installPackage(). """
|
||||
|
||||
if not hasattr(PandaModules, 'TiXmlDocument'):
|
||||
return
|
||||
|
||||
if self.updated:
|
||||
# If we've just installed a new version of the package,
|
||||
# re-measure the actual disk space used.
|
||||
self.diskSpace = self.__measureDiskSpace()
|
||||
|
||||
filename = Filename(self.getPackageDir(), self.UsageBasename)
|
||||
doc = TiXmlDocument(filename.toOsSpecific())
|
||||
if not doc.LoadFile():
|
||||
decl = TiXmlDeclaration("1.0", "utf-8", "")
|
||||
doc.InsertEndChild(decl)
|
||||
|
||||
xusage = doc.FirstChildElement('usage')
|
||||
if not xusage:
|
||||
doc.InsertEndChild(TiXmlElement('usage'))
|
||||
xusage = doc.FirstChildElement('usage')
|
||||
|
||||
now = int(time.time())
|
||||
|
||||
count = xusage.Attribute('count_app')
|
||||
try:
|
||||
count = int(count or '')
|
||||
except ValueError:
|
||||
count = 0
|
||||
xusage.SetAttribute('first_use', str(now))
|
||||
count += 1
|
||||
xusage.SetAttribute('count_app', str(count))
|
||||
|
||||
xusage.SetAttribute('last_use', str(now))
|
||||
|
||||
if self.updated:
|
||||
xusage.SetAttribute('last_update', str(now))
|
||||
self.updated = False
|
||||
else:
|
||||
# Since we haven't changed the disk space, we can just
|
||||
# read it from the previous xml file.
|
||||
diskSpace = xusage.Attribute('disk_space')
|
||||
try:
|
||||
diskSpace = int(diskSpace or '')
|
||||
except ValueError:
|
||||
# Unless it wasn't set already.
|
||||
self.diskSpace = self.__measureDiskSpace()
|
||||
|
||||
xusage.SetAttribute('disk_space', str(self.diskSpace))
|
||||
|
||||
# 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.getPackageDir().cStr(), '.xml')
|
||||
if doc.SaveFile(tfile.toOsSpecific()):
|
||||
tfile.renameTo(filename)
|
||||
|
||||
def getUsage(self):
|
||||
""" Returns the xusage element that is read from the usage.xml
|
||||
file, or None if there is no usage.xml file. """
|
||||
|
||||
if not hasattr(PandaModules, 'TiXmlDocument'):
|
||||
return None
|
||||
|
||||
filename = Filename(self.getPackageDir(), self.UsageBasename)
|
||||
doc = TiXmlDocument(filename.toOsSpecific())
|
||||
if not doc.LoadFile():
|
||||
return None
|
||||
|
||||
xusage = doc.FirstChildElement('usage')
|
||||
if not xusage:
|
||||
return None
|
||||
|
||||
return copy.copy(xusage)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
from pandac.PandaModules import VirtualFileSystem, VirtualFileMountSystem, Filename, TiXmlDocument
|
||||
vfs = VirtualFileSystem.getGlobalPtr()
|
||||
|
||||
class ScanDirectoryNode:
|
||||
""" This class is used to scan a list of files on disk. """
|
||||
|
||||
def __init__(self, pathname, ignoreUsageXml = False):
|
||||
self.pathname = pathname
|
||||
self.filenames = []
|
||||
self.fileSize = 0
|
||||
self.nested = []
|
||||
self.nestedSize = 0
|
||||
|
||||
xusage = None
|
||||
if not ignoreUsageXml:
|
||||
# Look for a usage.xml file in this directory. If we find
|
||||
# one, we read it for the file size and then stop here, as
|
||||
# an optimization.
|
||||
usageFilename = Filename(pathname, 'usage.xml')
|
||||
doc = TiXmlDocument(usageFilename.toOsSpecific())
|
||||
if doc.LoadFile():
|
||||
xusage = doc.FirstChildElement('usage')
|
||||
if xusage:
|
||||
diskSpace = xusage.Attribute('disk_space')
|
||||
try:
|
||||
diskSpace = int(diskSpace or '')
|
||||
except ValueError:
|
||||
diskSpace = None
|
||||
if diskSpace is not None:
|
||||
self.fileSize = diskSpace
|
||||
return
|
||||
|
||||
for vfile in vfs.scanDirectory(self.pathname):
|
||||
if hasattr(vfile, 'getMount'):
|
||||
if not isinstance(vfile.getMount(), VirtualFileMountSystem):
|
||||
# Not a real file; ignore it.
|
||||
continue
|
||||
|
||||
if vfile.isDirectory():
|
||||
# A nested directory.
|
||||
subdir = ScanDirectoryNode(vfile.getFilename(), ignoreUsageXml = ignoreUsageXml)
|
||||
self.nested.append(subdir)
|
||||
self.nestedSize += subdir.getTotalSize()
|
||||
|
||||
elif vfile.isRegularFile():
|
||||
# A nested file.
|
||||
self.filenames.append(vfile.getFilename())
|
||||
self.fileSize += vfile.getFileSize()
|
||||
|
||||
else:
|
||||
# Some other wacky file thing.
|
||||
self.filenames.append(vfile.getFilename())
|
||||
|
||||
if xusage:
|
||||
# Now update the usage.xml file with the newly-determined
|
||||
# disk space.
|
||||
xusage.SetAttribute('disk_space', str(self.getTotalSize()))
|
||||
tfile = Filename.temporary(pathname.cStr(), '.xml')
|
||||
if doc.SaveFile(tfile.toOsSpecific()):
|
||||
tfile.renameTo(usageFilename)
|
||||
|
||||
def getTotalSize(self):
|
||||
return self.nestedSize + self.fileSize
|
||||
|
||||
def extractSubdir(self, pathname):
|
||||
""" Finds the ScanDirectoryNode within this node that
|
||||
corresponds to the indicated full pathname. If it is found,
|
||||
removes it from its parent, and returns it. If it is not
|
||||
found, returns None. """
|
||||
|
||||
# We could be a little smarter here, but why bother. Just
|
||||
# recursively search all children.
|
||||
for subdir in self.nested:
|
||||
if subdir.pathname == pathname:
|
||||
self.nested.remove(subdir)
|
||||
self.nestedSize -= subdir.getTotalSize()
|
||||
return subdir
|
||||
|
||||
result = subdir.extractSubdir(pathname)
|
||||
if result:
|
||||
self.nestedSize -= result.getTotalSize()
|
||||
if subdir.getTotalSize() == 0:
|
||||
# No other files in the subdirectory that contains
|
||||
# this package; remove it too.
|
||||
self.nested.remove(subdir)
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -13,6 +13,8 @@
|
|||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "find_root_dir.h"
|
||||
#include "mkdir_complete.h"
|
||||
#include "get_tinyxml.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
|
|
@ -35,27 +37,6 @@ DEFINE_KNOWN_FOLDER(FOLDERID_InternetCache, 0x352481E8, 0x33BE, 0x4251, 0xBA, 0x
|
|||
#endif // _WIN32
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: check_root_dir
|
||||
// Description: Tests the proposed root dir string for validity.
|
||||
// Returns true if Panda3D can be successfully created
|
||||
// within the dir, false otherwise.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
static bool
|
||||
check_root_dir(const string &root) {
|
||||
// Attempt to make it first, if possible.
|
||||
CreateDirectory(root.c_str(), NULL);
|
||||
|
||||
bool isdir = false;
|
||||
DWORD results = GetFileAttributes(root.c_str());
|
||||
if (results != -1) {
|
||||
isdir = (results & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
}
|
||||
return isdir;
|
||||
}
|
||||
#endif // _WIN32
|
||||
|
||||
#ifdef _WIN32
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: get_csidl_dir
|
||||
|
|
@ -71,7 +52,7 @@ get_csidl_dir(int csidl) {
|
|||
string root = buffer;
|
||||
root += string("/Panda3D");
|
||||
|
||||
if (check_root_dir(root)) {
|
||||
if (mkdir_complete(root, cerr)) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
|
@ -109,12 +90,12 @@ wstr_to_string(string &result, const LPWSTR wstr) {
|
|||
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: find_root_dir
|
||||
// Description: Returns the path to the installable Panda3D directory
|
||||
// on the user's machine.
|
||||
// Function: find_root_dir_default
|
||||
// Description: Returns the path to the system-default for the root
|
||||
// directory. This is where we look first.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
string
|
||||
find_root_dir() {
|
||||
static string
|
||||
find_root_dir_default() {
|
||||
#ifdef _WIN32
|
||||
// First, use IEIsProtectedModeProcess() to determine if we are
|
||||
// running in IE's "protected mode" under Vista.
|
||||
|
|
@ -165,7 +146,7 @@ find_root_dir() {
|
|||
CoTaskMemFree(cache_path);
|
||||
|
||||
root += string("/Panda3D");
|
||||
if (check_root_dir(root)) {
|
||||
if (mkdir_complete(root, cerr)) {
|
||||
FreeLibrary(shell32);
|
||||
FreeLibrary(ieframe);
|
||||
return root;
|
||||
|
|
@ -199,7 +180,7 @@ find_root_dir() {
|
|||
} else {
|
||||
CoTaskMemFree(cache_path);
|
||||
root += string("/Panda3D");
|
||||
if (check_root_dir(root)) {
|
||||
if (mkdir_complete(root, cerr)) {
|
||||
FreeLibrary(ieframe);
|
||||
return root;
|
||||
}
|
||||
|
|
@ -236,7 +217,7 @@ find_root_dir() {
|
|||
if (GetTempPath(buffer_size, buffer) != 0) {
|
||||
root = buffer;
|
||||
root += string("Panda3D");
|
||||
if (check_root_dir(root)) {
|
||||
if (mkdir_complete(root, cerr)) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
|
@ -273,3 +254,42 @@ find_root_dir() {
|
|||
return ".";
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: find_root_dir
|
||||
// Description: Returns the path to the installable Panda3D directory
|
||||
// on the user's machine.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
string
|
||||
find_root_dir() {
|
||||
string root = find_root_dir_default();
|
||||
|
||||
// Now look for a config.xml file in that directory, which might
|
||||
// redirect us elsewhere.
|
||||
string config_filename = root + "/config.xml";
|
||||
TiXmlDocument doc(config_filename);
|
||||
if (!doc.LoadFile()) {
|
||||
// No config.xml found, or not valid xml.
|
||||
return root;
|
||||
}
|
||||
|
||||
TiXmlElement *xconfig = doc.FirstChildElement("config");
|
||||
if (xconfig == NULL) {
|
||||
// No <config> element within config.xml.
|
||||
return root;
|
||||
}
|
||||
|
||||
const char *new_root = xconfig->Attribute("root_dir");
|
||||
if (new_root == NULL || *new_root == '\0') {
|
||||
// No root_dir specified.
|
||||
return root;
|
||||
}
|
||||
|
||||
if (!mkdir_complete(new_root, cerr)) {
|
||||
// The specified root_dir wasn't valid.
|
||||
return root;
|
||||
}
|
||||
|
||||
// We've been redirected to another location. Respect that.
|
||||
return new_root;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -394,6 +394,26 @@ get_package_desc_file(FileSpec &desc_file, // out
|
|||
return false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: P3DHost::forget_package
|
||||
// Access: Public
|
||||
// Description: Removes the indicated package from the cache of
|
||||
// packages known by this host. This is invoked from
|
||||
// the Python side by AppRunner.deletePackages(), so
|
||||
// that we remove the package before deleting its files.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
void P3DHost::
|
||||
forget_package(P3DPackage *package, const string &alt_host) {
|
||||
string key = package->get_package_name() + "_" + package->get_package_version();
|
||||
|
||||
PackageMap &package_map = _packages[alt_host];
|
||||
|
||||
// Hmm, this is a memory leak. But we allow it to remain, since
|
||||
// it's an unusual circumstance (uninstalling), and it's safer to
|
||||
// leak than to risk a floating pointer.
|
||||
package_map.erase(key);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: P3DHost::migrate_package
|
||||
// Access: Public
|
||||
|
|
@ -494,6 +514,7 @@ uninstall() {
|
|||
|
||||
P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
|
||||
inst_mgr->delete_directory_recursively(_host_dir);
|
||||
inst_mgr->forget_host(this);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ public:
|
|||
const string &package_name,
|
||||
const string &package_version);
|
||||
|
||||
void forget_package(P3DPackage *package, const string &alt_host = "");
|
||||
void migrate_package(P3DPackage *package, const string &alt_host, P3DHost *new_host);
|
||||
|
||||
void choose_random_mirrors(vector<string> &result, int num_mirrors);
|
||||
|
|
|
|||
|
|
@ -1473,8 +1473,8 @@ void P3DInstance::
|
|||
uninstall_host() {
|
||||
uninstall_packages();
|
||||
|
||||
// Collect the set of hosts referenced by this instance.
|
||||
set<P3DHost *> hosts;
|
||||
|
||||
Packages::const_iterator pi;
|
||||
for (pi = _packages.begin(); pi != _packages.end(); ++pi) {
|
||||
P3DPackage *package = (*pi);
|
||||
|
|
@ -1484,6 +1484,7 @@ uninstall_host() {
|
|||
}
|
||||
}
|
||||
|
||||
// Uninstall all of them.
|
||||
set<P3DHost *>::iterator hi;
|
||||
for (hi = hosts.begin(); hi != hosts.end(); ++hi) {
|
||||
P3DHost *host = (*hi);
|
||||
|
|
@ -3098,6 +3099,7 @@ report_package_done(P3DPackage *package, bool success) {
|
|||
nout << "No <config> entry in image package\n";
|
||||
return;
|
||||
}
|
||||
package->mark_used();
|
||||
|
||||
for (int i = 0; i < (int)IT_none; ++i) {
|
||||
if (_image_files[i]._use_standard_image) {
|
||||
|
|
@ -3129,6 +3131,8 @@ report_package_done(P3DPackage *package, bool success) {
|
|||
// failing to download it) means we can finish checking the
|
||||
// authenticity of the p3d file.
|
||||
|
||||
package->mark_used();
|
||||
|
||||
// Take down the download progress.
|
||||
if (_splash_window != NULL) {
|
||||
_splash_window->set_install_progress(0.0, true, 0);
|
||||
|
|
@ -3145,6 +3149,8 @@ report_package_done(P3DPackage *package, bool success) {
|
|||
// Another special case: successfully downloading p3dcert means we
|
||||
// can enable the auth button.
|
||||
|
||||
package->mark_used();
|
||||
|
||||
// Take down the download progress.
|
||||
if (_splash_window != NULL) {
|
||||
_splash_window->set_install_progress(0.0, true, 0);
|
||||
|
|
|
|||
|
|
@ -614,6 +614,21 @@ get_host(const string &host_url) {
|
|||
return host;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: P3DInstanceManager::forget_host
|
||||
// Access: Public
|
||||
// Description: Removes the indicated host from the cache.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
void P3DInstanceManager::
|
||||
forget_host(P3DHost *host) {
|
||||
const string &host_url = host->get_host_url();
|
||||
|
||||
// Hmm, this is a memory leak. But we allow it to remain, since
|
||||
// it's an unusual circumstance (uninstalling), and it's safer to
|
||||
// leak than to risk a floating pointer.
|
||||
_hosts.erase(host_url);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: P3DInstanceManager::get_unique_id
|
||||
// Access: Public
|
||||
|
|
@ -1395,8 +1410,9 @@ nt_thread_run() {
|
|||
P3DInstance *inst = (*ni);
|
||||
assert(inst != NULL);
|
||||
P3D_request_ready_func *func = inst->get_request_ready_func();
|
||||
assert(func != NULL);
|
||||
(*func)(inst);
|
||||
if (func != NULL) {
|
||||
(*func)(inst);
|
||||
}
|
||||
}
|
||||
_notify_ready.acquire();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ public:
|
|||
void wait_request(double timeout);
|
||||
|
||||
P3DHost *get_host(const string &host_url);
|
||||
void forget_host(P3DHost *host);
|
||||
|
||||
inline int get_num_instances() const;
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ P3DPackage(P3DHost *host, const string &package_name,
|
|||
_failed = false;
|
||||
_active_download = NULL;
|
||||
_saved_download = NULL;
|
||||
_updated = false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -210,6 +211,70 @@ remove_instance(P3DInstance *inst) {
|
|||
begin_info_download();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: P3DPackage::mark_used
|
||||
// Access: Public
|
||||
// Description: Marks this package as having been "used", for
|
||||
// accounting purposes.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
void P3DPackage::
|
||||
mark_used() {
|
||||
// Unlike the Python variant of this function, we don't mess around
|
||||
// with updating the disk space or anything.
|
||||
string filename = get_package_dir() + "/usage.xml";
|
||||
TiXmlDocument doc(filename);
|
||||
if (!doc.LoadFile()) {
|
||||
TiXmlDeclaration *decl = new TiXmlDeclaration("1.0", "utf-8", "");
|
||||
doc.LinkEndChild(decl);
|
||||
}
|
||||
|
||||
TiXmlElement *xusage = doc.FirstChildElement("usage");
|
||||
if (xusage == NULL) {
|
||||
xusage = new TiXmlElement("usage");
|
||||
doc.LinkEndChild(xusage);
|
||||
}
|
||||
|
||||
time_t now = time(NULL);
|
||||
int count = 0;
|
||||
xusage->Attribute("count_runtime", &count);
|
||||
if (count == 0) {
|
||||
xusage->SetAttribute("first_use", now);
|
||||
}
|
||||
|
||||
++count;
|
||||
xusage->SetAttribute("count_runtime", count);
|
||||
xusage->SetAttribute("last_use", now);
|
||||
|
||||
if (_updated) {
|
||||
// If we've updated the package, we're no longer sure what its
|
||||
// disk space is. Remove that from the XML file, so that the
|
||||
// Python code can recompute it later.
|
||||
xusage->RemoveAttribute("disk_space");
|
||||
xusage->SetAttribute("last_update", now);
|
||||
}
|
||||
|
||||
// Write the file to a temporary filename, then atomically move it
|
||||
// to its actual filename, to avoid race conditions.
|
||||
ostringstream strm;
|
||||
strm << get_package_dir() << "/usage_";
|
||||
#ifdef _WIN32
|
||||
strm << GetCurrentProcessId();
|
||||
#else
|
||||
strm << getpid();
|
||||
#endif
|
||||
strm << ".xml";
|
||||
string tfile = strm.str();
|
||||
|
||||
unlink(tfile.c_str());
|
||||
if (doc.SaveFile(tfile)) {
|
||||
if (rename(tfile.c_str(), filename.c_str()) != 0) {
|
||||
// If rename failed, remove the original file first.
|
||||
unlink(filename.c_str());
|
||||
rename(tfile.c_str(), filename.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: P3DPackage::uninstall
|
||||
// Access: Public
|
||||
|
|
@ -247,6 +312,9 @@ uninstall() {
|
|||
_ready = false;
|
||||
_failed = false;
|
||||
_allow_data_download = false;
|
||||
|
||||
// Make sure the host forgets us too.
|
||||
_host->forget_package(this);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -707,6 +775,7 @@ got_desc_file(TiXmlDocument *doc, bool freshly_downloaded) {
|
|||
|
||||
inst_mgr->remove_file_from_list(contents, _desc_file_basename);
|
||||
inst_mgr->remove_file_from_list(contents, _uncompressed_archive.get_filename());
|
||||
inst_mgr->remove_file_from_list(contents, "usage.xml");
|
||||
Extracts::iterator ei;
|
||||
for (ei = _extracts.begin(); ei != _extracts.end(); ++ei) {
|
||||
inst_mgr->remove_file_from_list(contents, (*ei).get_filename());
|
||||
|
|
@ -727,6 +796,7 @@ got_desc_file(TiXmlDocument *doc, bool freshly_downloaded) {
|
|||
chmod(pathname.c_str(), 0644);
|
||||
#endif
|
||||
unlink(pathname.c_str());
|
||||
_updated = true;
|
||||
}
|
||||
|
||||
// Verify the uncompressed archive.
|
||||
|
|
@ -1509,6 +1579,7 @@ do_step(bool download_finished) {
|
|||
|
||||
if (_download->get_download_success()) {
|
||||
// The Download object has already validated the hash.
|
||||
_package->_updated = true;
|
||||
return IT_step_complete;
|
||||
|
||||
} else if (_download->get_download_terminated()) {
|
||||
|
|
@ -1801,6 +1872,7 @@ thread_step() {
|
|||
unlink(source_pathname.c_str());
|
||||
|
||||
// All done uncompressing.
|
||||
_package->_updated = true;
|
||||
return IT_step_complete;
|
||||
}
|
||||
|
||||
|
|
@ -1847,6 +1919,7 @@ thread_step() {
|
|||
return IT_step_failed;
|
||||
}
|
||||
|
||||
_package->_updated = true;
|
||||
return IT_step_complete;
|
||||
}
|
||||
|
||||
|
|
@ -1901,6 +1974,7 @@ thread_step() {
|
|||
return IT_step_failed;
|
||||
}
|
||||
|
||||
_package->_updated = true;
|
||||
return IT_step_complete;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ public:
|
|||
void add_instance(P3DInstance *inst);
|
||||
void remove_instance(P3DInstance *inst);
|
||||
|
||||
void mark_used();
|
||||
void uninstall();
|
||||
|
||||
TiXmlElement *make_xml();
|
||||
|
|
@ -290,6 +291,7 @@ private:
|
|||
|
||||
size_t _unpack_size;
|
||||
Extracts _extracts;
|
||||
bool _updated;
|
||||
|
||||
static const double _download_factor;
|
||||
static const double _uncompress_factor;
|
||||
|
|
|
|||
|
|
@ -958,6 +958,8 @@ start_p3dpython(P3DInstance *inst) {
|
|||
_env += string("_ROOT=");
|
||||
_env += package->get_package_dir();
|
||||
_env += '\0';
|
||||
|
||||
package->mark_used();
|
||||
}
|
||||
|
||||
// Check for a few tokens that have special meaning at this level.
|
||||
|
|
|
|||
Loading…
Reference in New Issue