import json, logging, re, time, urllib, zipfile try: import tarfile except: tarfile = None import os, sys, shutil try: from pnc.attrdict import AttrDict except ImportError: # Fall back on the old location - just in case from pnc.dep.attrdict import AttrDict logger = logging.getLogger(__name__) USER_TYPE = "user" # user - for normal people # beta - for the original beta testers # dev - used to be for git users, now it's anyone with the 3.41 beta # edge - new git stuff. bleeding edge, do not try at home (kiooeht version) INSTALL_TYPE = "installer" # installer - Windows/Mac installer (exe/dmg) # zip - Windows zip (zip) # source - Win/Linux/Mac source code (zip/tar) OS_TYPE = sys.platform # win32, linux, darwin if OS_TYPE.startswith("linux"): OS_TYPE = "linux" elif OS_TYPE == "darwin": OS_TYPE = "mac" # These will eventually be phased out _pcMajor = "3.41" _pcMinor = "4" _pcStatus = "A" # A = alpha # B = beta # RC = release candidate # None = public release _pcRevision = "13" _pcVersion = "" _updateCheckURL = "https://github.com/karxi/pesterchum/raw/master/VERSION.js" _downloadURL = "https://github.com/karxi/pesterchum/archive/master.zip" jsodeco = json.JSONDecoder() # Whether or not we've completed an update (requires a restart). has_updated = False def _py_version_check(): import sys # == Python Version Checking == # Check that we're running the right version of Python. # This is the version we need. pyreq = {"major": 2, "minor": 7} # Just some preprocessing to make formatting the version info a little # easier. Note that the sys.version_info type doesn't convert to dict, # despite having named indices like a namedtuple, so we have to do it # manually. # This is the version we have. pyver = dict(zip(("major", "minor"), sys.version_info[:2])) # Compose the base of an error message that we may use in the future. errmsg = "ERROR: Pesterchum is designed to be run on Python " + \ "{major}.{minor}.".format(**pyreq) errmsg = [ errmsg ] errmsg.append("It is not designed for use with Python {major}.{minor}.") # Now we have a list that we can further process into a more specific # error message. if pyver["major"] > pyreq["major"]: # We're using Python 3, which this script won't work with. errmsg = errmsg.extend([ "Due to syntax differences,", "it cannot be run with this version of Python." ]) errmsg = ' '.join(errmsg) errmsg = errmsg.format(**pyver) logger.critical(errmsg) sys.exit() elif pyver["major"] != pyreq["major"] or pyver["minor"] < pyreq["minor"]: # We're either not running Python 2 (we have something earlier?!) or # we're below the minimum required minor version (e.g. 2.6 or 2.4 or # similar). # This means that we wouldn't have certain syntax improvements that we # need - like inline generators, 'with' statements, lambdas, etc. # NOTE: This MAY be lowered to 2.6 later, since there's little # difference. errmsg = errmsg.extend([ "This version of Python lacks certain features", "that are necessary for it to run." ]) errmsg = ' '.join(errmsg) errmsg = errmsg.format(**pyver) logger.critical(errmsg) sys.exit() # Not 100% finished - certain output formats seem odd def get_pchum_ver(raw=0, pretty=False, file=None, use_hard_coded=None): # If use_hard_coded is None, we don't care. If it's False, we won't use it. getrawlines = lambda fobj: [ x.strip() for x in fobj.readlines() ] if file: # Don't fall back onto defaults if we were given a file. use_hard_coded = False try: if use_hard_coded: # This is messy code, but we just want it to work for now. raise ValueError if file: # Leave closing this to the caller. raw_ver = getrawlines(file) else: # Open our default file ourselves. with open("VERSION.js", 'r') as fo: raw_ver = getrawlines(fo) raw_ver = ' '.join(raw_ver) # Now that we have the actual version, we can just set everything up # neatly. ver = jsodeco.decode(raw_ver) ver = AttrDict( (k.encode('ascii'), v) for k, v in ver.items() ) # Do a bit of compensation for the unicode part of JSON. ver.status, ver.utype = str(ver.status), str(ver.utype) except: if use_hard_coded == False: # We refuse to use the hard-coded values, period. raise global _pcMajor, _pcMinor, _pcStatus, _pcRevision, USER_TYPE ver = AttrDict({ "major": _pcMajor, "minor": _pcMinor, "status": _pcStatus, "rev": _pcRevision, "utype": USER_TYPE }) ver.major = float(ver.major) ver.minor = int(ver.minor) if not ver.status: ver.status = None ver.rev = int(ver.rev) if raw: if raw > 1: # Give the AttrDict. return ver else: # Give a tuple. return (ver.major, ver.minor, ver.status, ver.rev, ver.utype) # Compose the version information into a string. # We usually specify the format for this pretty strictly. # We wnat it to look like "3.14.01-A07", for example. elif pretty: if pretty > True: # True == 1; we get here if pretty is greater than 1 if ver.utype == "edge": # If this is an edge build, the other types don't really # matter. ver.status = "Bleeding Edge" else: statuses = { # These are slightly unnecessary, but.... "A": "Alpha", "B": "Beta", "RC": "Release Candidate" } # Pick a status or don't give one. ver.status = statuses.get(ver.status, "") if ver.status: ver.status = " " + ver.status # Not the same as the original output, but it seems nicer. retval = "{major:.2f}.{minor:02d}{status!s} {rev:02d}" else: retval = "{major:.2f}.{minor:02d}-r{rev:02d}{status!s} ({utype!s})" elif ver.status: retval = "{major:.2f}.{minor:02d}-{status!s}{rev:02d}" else: retval = "{major:.2f}.{minor:02d}.{rev:02d}" return retval.format(**ver) def pcVerCalc(): global _pcVersion # The logic for this has been moved for the sake of ease of use. _pcVersion = get_pchum_ver(raw=False) def lexVersion(short=False): if not _pcStatus: return "%s.%s" % (_pcMajor, _pcMinor) utype = "" if USER_TYPE == "edge": utype = "E" if short: return "%s.%s%s%s%s" % (_pcMajor, _pcMinor, _pcStatus, _pcRevision, utype); stype = "" if _pcStatus == "A": stype = "Alpha" elif _pcStatus == "B": stype = "Beta" elif _pcStatus == "RC": stype = "Release Candidate" if utype == "E": utype = " Bleeding Edge" return "%s.%s %s %s%s" % (_pcMajor, _pcMinor, stype, _pcRevision, utype); # Naughty I know, but it lets me grab it from the bash script. if __name__ == "__main__": print lexVersion() def verStrToNum(ver): w = re.match("(\d+\.?\d+)\.(\d+)-?([A-Za-z]{0,2})\.?(\d*):(\S+)", ver) if not w: print "Update check Failure: 3"; return full = ver[:ver.find(":")] return full,w.group(1),w.group(2),w.group(3),w.group(4),w.group(5) def is_outdated(url=None): if not url: global _updateCheckURL url = _updateCheckURL # karxi: Do we really need to sleep here? Why? time.sleep(3) try: jsfile = urllib.urlopen(_updateCheckURL) gitver = get_pchum_ver(raw=2, file=jsfile) except: # No error handling yet.... raise finally: jsfile.close() ourver = get_pchum_ver(raw=2) # Now we can compare. outdated = False # What, if anything, tipped us off trigger = None keys = ("major", "minor", "rev", "status") for k in keys: if gitver[k] > ourver[k]: # We don't test for 'bleeding edge' just yet. trigger = k outdated = True if outdated: logger.info( "Out of date (newer is {0!r} {1} to our {2})".format( trigger, gitver[trigger], ourver[trigger])) return outdated # So now all that's left to do is to set up the actual downloading of # updates...or at least a notifier, until it can be automated. def updatePesterchum(url=None): # TODO: This is still WIP; the actual copying needs to be adjusted. if url is None: global _downloadURL url = _downloadURL try: # Try to fetch the update. fn, fninfo = urllib.urlretrieve(url) except urllib.ContentTooShortError: # Our download was interrupted; there's not really anything we can do # here. raise ext = osp.splitext(fn) if ext == ".zip": import zipfile is_updatefile = zipfile.is_zipfile openupdate = zipfile.ZipFile elif tarfile and ext.startswith(".tar"): import tarfile is_updatefile = tarfile.is_tarfile openupdate = tarfile.open else: logger.info("No handler available for update {0!r}".format(fn)) return logger.info("Opening update {0!s} {1!r} ...".format(ext, fn)) if is_updatefile(fn): update = openupdate(fn, 'r') tmpfldr, updfldr = "tmp", "update" # Set up the folder structure. if osp.exists(updfldr): # We'll need this later. shutil.rmtree(updfldr) if osp.exists(tmpfldr): shutil.rmtree(tmpfldr) os.mkdir(tmpfldr) update.extractall(tmpfldr) contents = os.listdir(tmpfldr) # Is there only one folder here? Git likes to do this with repos. # If there is, move it to our update folder. # If there isn't, move the temp directory to our update folder. if len(tmpcts) == 1: arcresult = osp.join(tmpfldr, contents[0]) if osp.isdir(arcresult): shutil.move(arcresult, updfldr) else: shutil.move(tmpfldr, updfldr) # Remove the temporary folder. os.rmdir(tmpfldr) # Remove the update file. os.remove(fn) # ... What does this even do? It recurses.... removeCopies(updfldr) # Why do these both skip the first seven characters?! copyUpdate(updfldr) # Finally, remove the update folder. shutil.rmtree(updfldr) def updateCheck(q): # karxi: Disabled for now; causing issues. # There should be an alternative system in place soon. return q.put((False,0)) time.sleep(3) data = urllib.urlencode({"type" : USER_TYPE, "os" : OS_TYPE, "install" : INSTALL_TYPE}) try: f = urllib.urlopen("http://distantsphere.com/pesterchum.php?" + data) except: print "Update check Failure: 1"; return q.put((False,1)) newest = f.read() f.close() if not newest or newest[0] == "<": print "Update check Failure: 2"; return q.put((False,2)) try: (full, major, minor, status, revision, url) = verStrToNum(newest) except TypeError: return q.put((False,3)) print full print repr(verStrToNum(newest)) if major <= _pcMajor: if minor <= _pcMinor: if status: if status <= _pcStatus: if revision <= _pcRevision: return q.put((False,0)) else: if not _pcStatus: if revision <= _pcRevision: return q.put((False,0)) print "A new version of Pesterchum is avaliable!" q.put((full,url)) def removeCopies(path): for f in os.listdir(path): filePath = osp.join(path, f) trunc, rem = filePath[:7], filePath[7:] if not osp.isdir(filePath): if osp.exists(rem): logger.debug( "{0: <4}Deleting copy: {1!r} >{2!r}<".format( '', trunc, rem) ) os.remove(rem) else: # Recurse removeCopies(filePath) def copyUpdate(path): for f in os.listdir(path): filePath = osp.join(path, f) trunc, rem = filePath[:7], filePath[7:] if not osp.isdir(filePath): logger.debug( "{0: <4}Making copy: {1!r} ==> {2!r}".format( '', filePath, rem) ) shutil.copy2(filePath, rem) else: if not osp.exists(rem): os.mkdir(rem) # Recurse copyUpdate(filePath) def updateExtract(url, extension): if extension: fn = "update" + extension urllib.urlretrieve(url, fn) else: fn = urllib.urlretrieve(url)[0] if tarfile and tarfile.is_tarfile(fn): extension = ".tar.gz" elif zipfile.is_zipfile(fn): extension = ".zip" else: try: from libs import magic # :O I'M IMPORTING /MAGIC/!! HOLY SHIT! mime = magic.from_file(fn, mime=True) if mime == 'application/octet-stream': extension = ".exe" except: pass print url, fn, extension if extension == ".exe": pass elif extension == ".zip" or extension.startswith(".tar"): if extension == ".zip": from zipfile import is_zipfile as is_updatefile, ZipFile as openupdate print "Opening .zip" elif tarfile and extension.startswith(".tar"): from tarfile import is_tarfile as is_updatefile, open as openupdate print "Opening .tar" else: return if is_updatefile(fn): update = openupdate(fn, 'r') if os.path.exists("tmp"): shutil.rmtree("tmp") os.mkdir("tmp") update.extractall("tmp") tmp = os.listdir("tmp") if os.path.exists("update"): shutil.rmtree("update") if len(tmp) == 1 and \ os.path.isdir("tmp/"+tmp[0]): shutil.move("tmp/"+tmp[0], "update") else: shutil.move("tmp", "update") os.rmdir("tmp") os.remove(fn) removeCopies("update") copyUpdate("update") shutil.rmtree("update") def updateDownload(url): extensions = [".exe", ".zip", ".tar.gz", ".tar.bz2"] found = False for e in extensions: if url.endswith(e): found = True updateExtract(url, e) if not found: if url.startswith("https://github.com/") and url.count('/') == 4: updateExtract(url+"/tarball/master", None) else: updateExtract(url, None)