# -*- test-case-name: mv3d.test.util.test_patch -*- # Copyright (C) 2010-2012 Mortal Coil Games # See LICENSE for details. """ Automated patching for MV3D clients. @author: mike """ from ConfigParser import ConfigParser from urlparse import urlparse import httplib import os import zipfile import sys try: from cStringIO import StringIO except ImportError: from StringIO import StringIO from hashlib import sha1 class Patcher(object): """ Handles patching MV3D up to date. Use *before* other modules are loaded. """ def __init__(self): self.patches = ConfigParser() self.loadedFiles = [] self.baseDir = os.path.abspath(os.path.expanduser("~/.mv3d/test")) def _getPage(self, url): """ Download a single url and return the data """ urlChunks = urlparse(url) _scheme, host, path, _parameters, _query, _fragment = urlChunks connection = httplib.HTTPConnection(host, 80) connection.connect() connection.request("GET", path) response = connection.getresponse() if response.status < 200 or response.status > 299: raise ValueError("Bad http response: %d" % response.status) data = response.read() connection.close() return data def downloadPatchFile(self, url): """ Downloads a patch file. """ data = self._getPage(url) self.patches.readfp(StringIO(data)) self.loadedFiles.append(url) def gatherDependencyList(self): """ Check the dependencies of the patch files loaded and gather patch files from dependents. """ missingPatchFiles = True while missingPatchFiles: deps = [] for section in self.patches.sections(): if self.patches.has_option(section, "Dependencies"): deps.extend(self.patches.get(section, "Dependencies").split( ",")) missingPatchFiles = False for dep in deps[:]: if not dep in self.loadedFiles and dep: self.downloadPatchFile(dep) deps.remove(dep) missingPatchFiles = True def checkCRC(self, localFile, crc): """ Check the CRC of a file against the one specified """ fil = open(localFile, "rb") hash = sha1() count = 0 while True: count += 1 data = fil.read(2 ** 20) if not data: break hash.update(data) fil.close() if hash.hexdigest() == crc: return True return False def getMissingFiles(self): """ Figure out what files are missing. """ self.gatherDependencyList() files = {} for section in self.patches.sections(): if self.patches.has_option(section, "Download"): download = dict(url=self.patches.get(section, "Download")) download["localfile"] = os.path.join(self.baseDir, download["url"].split("/")[-1]) if os.path.exists(download["localfile"]): if self.patches.has_option(section, "CRC"): download["CRC"] = self.patches.get(section, "CRC") download["needed"] = not self.checkCRC( download["localfile"], download["CRC"]) else: download["needed"] = False else: download["needed"] = True if self.patches.has_option(section, "Unzip"): download["unzip"] = self.patches.getboolean(section, "Unzip") else: download["unzip"] = True if not download["needed"] and not download["unzip"]: sys.path.append(download["localfile"]) files[section] = download return [(section, file) for section, file in files.items() if file["needed"]] def download(self): """ Calculate current dependencies and download everything. """ if not os.path.exists(self.baseDir): os.makedirs(self.baseDir) files = self.getMissingFiles() for _section, file in files: data = self._getPage(file["url"]) fil = open(file["localfile"], "wb") fil.write(data) fil.close() if file["unzip"]: sio = StringIO(data) zippy = zipfile.ZipFile(sio, "r") try: zippy.extractall(self.baseDir) except AttributeError: #extractall = 2.6 #http://stackoverflow.com/questions/1774434/download-a-zip-file-to-a-local-drive-and-extract-all-files-to-a-destination-folde for n in zippy.namelist(): dest = os.path.join(self.baseDir, n) destdir = os.path.dirname(dest) if not os.path.isdir(destdir): os.makedirs(destdir) data = zippy.read(n) f = open(dest, 'wb') f.write(data) f.close() zippy.close() else: sys.path.append(file["localfile"]) def run(self): """ Find which section has the ExecFunction attribute and exec it. """ results = [] sys.path.insert(0, self.baseDir) os.chdir(self.baseDir) try: # TODO: this is probably never going to work. reactorName = base.appRunner.getToken("reactor") except: reactorName = "wx" if reactorName == "wx": from twisted.internet import wxreactor wxreactor.install() else: from twisted.internet import selectreactor selectreactor.install() for section in self.patches.sections(): if self.patches.has_option(section, "ExecFunction"): fullPath = self.patches.get(section, "ExecFunction") fname = fullPath.split(".")[-1] mname = ".".join(fullPath.split(".")[:-1]) mod = __import__(mname, fromlist=[fname]) func = getattr(mod, fname) if self.patches.has_option(section, "ExecArgs"): args = self.patches.get(section, "ExecArgs").split(",") else: args = [] results.append(func(*args)) return results