diff --git a/Actions/actions.py b/Actions/actions.py index b8f74993..876b492e 100644 --- a/Actions/actions.py +++ b/Actions/actions.py @@ -128,9 +128,9 @@ def processAction(self, msg): else: self.data.ui_queue1.put("Alert", "Alert", "No GCode file loaded.") elif msg["data"]["command"] == "update": - if not self.data.releaseManager.update(msg["data"]["arg"]): - self.data.ui_queue1.put("Alert", "Alert", "Error with updating webcontrol.") - return "Shutdown" + # Errors handled during update so only reboot on success. + if self.data.releaseManager.update(msg["data"]["arg"]): + return "Shutdown" elif msg["data"]["command"] == "cutTriangularCalibrationPattern": if not self.data.triangularCalibration.cutTriangularCalibrationPattern(): self.data.ui_queue1.put("Alert", "Alert", "Error with cutting triangular calibration pattern.") @@ -1989,7 +1989,7 @@ def checkForLatestPyRelease(self): except: print("error parsing tagname") print(latest) - if latest>self.data.pyInstallCurrentVersion: + if latest>self.data.pyInstallCurrentVersionNumber: if latestRelease is not None: print(latestRelease.tag_name) assets = latestRelease.get_assets() diff --git a/Background/messageProcessor.py b/Background/messageProcessor.py index 6f30bec8..4e0d3347 100644 --- a/Background/messageProcessor.py +++ b/Background/messageProcessor.py @@ -18,7 +18,7 @@ def start(self): # check for available update file every hour. if time.time()-self.data.lastChecked > 60*60: self.data.lastChecked = time.time() - self.data.releaseManager.checkForLatestPyRelease() + self.data.releaseManager.checkLatestRelease(True) self.data.helpManager.checkForUpdatedHelp() # process messages while queue is not empty. Everything else is on hold until queue is cleared. while ( not self.data.message_queue.empty() ): # if there is new data to be read diff --git a/DataStructures/data.py b/DataStructures/data.py index 316b109f..79ac3c73 100644 --- a/DataStructures/data.py +++ b/DataStructures/data.py @@ -4,7 +4,7 @@ from DataStructures.uiQueue import UIQueue from config.config import Config import queue - +import datetime class Data: """ @@ -38,7 +38,8 @@ class Data: Version Updater ''' lastChecked = -1 - pyInstallCurrentVersion = 0.94 + pyInstallCurrentVersionNumber = 0.94 + pyInstallCurrentVersionDate = datetime.datetime(2020, 5, 21) pyInstallUpdateAvailable = False pyInstallUpdateBrowserUrl = "" pyInstallUpdateVersion = 0 diff --git a/ReleaseManager/releaseManager.py b/ReleaseManager/releaseManager.py index 02440382..78ae6902 100644 --- a/ReleaseManager/releaseManager.py +++ b/ReleaseManager/releaseManager.py @@ -9,8 +9,10 @@ from github import Github import wget import subprocess -from shutil import copyfile - +import shutil +import datetime +import sys +from app import socketio class ReleaseManager(MakesmithInitFuncs): @@ -20,48 +22,84 @@ def __init__(self): releases = None latestRelease = None - def getReleases(self): - tempReleases = [] - enableExperimental = self.data.config.getValue("WebControl Settings", "experimentalReleases") - for release in self.releases: - if not enableExperimental: - if not self.isExperimental(re.sub(r'[v]', r'', release.tag_name)): + def fetchReleases(self): + try: + print("Fetching GitHub releases.") + g = Github() + repo = g.get_repo("WebControlCNC/WebControl") + self.releases = repo.get_releases() + for release in self.releases: + release.tag_version = 0 + release.tag_date = datetime.datetime(1, 1, 1, 0, 0) + + versionPattern = re.compile(r"^v?([0-9.]+)_?([0-9]{4}-[0-9]{2}-[0-9]{2})?$") + versionInfo = versionPattern.match(release.tag_name) + if (versionInfo): + release.tag_version = float(versionInfo.group(1)) + if (versionInfo.group(2)): + release.tag_date = datetime.datetime.strptime("%Y-%m-%d", versionInfo.group(2)) + else: + release.tag_date = release.published_at + else: + # This release tage name is not valid. + print("Release tag invlaid: " + release.tag_name) + # For testing of temp release. + if (release.tag_name == "2020-05-26-2021"): + release.tag_version = 0.94 + release.tag_date = datetime.datetime(2020, 5, 26) + + except Exception as e: + print("Error fetching github releases: " + str(e)) + + def getValidReleases(self, refresh = False): + if (self.releases is None or refresh): + self.fetchReleases() + + try: + print("Getting valid releases.") + enablePreRelease = self.data.config.getValue("WebControl Settings", "experimentalReleases") + tempReleases = [] + for release in self.releases: + if release.prerelease: + if enablePreRelease: + # Add experimental releases is setting enabled. + tempReleases.append(release) + else: + # Add stable releases. tempReleases.append(release) - else: - tempReleases.append(release) - return tempReleases + return tempReleases + + except Exception as e: + print("Error getting valid releases: " + str(e)) + def getLatestValidRelease(self, refresh = False): + if (self.latestRelease == None or refresh): + self.checkLatestRelease() - def getLatestRelease(self): return self.latestRelease - def checkForLatestPyRelease(self): + def checkLatestRelease(self, refresh = False): if True: # self.data.platform=="PYINSTALLER": - print("Checking latest pyrelease.") try: - enableExperimental = self.data.config.getValue("WebControl Settings", "experimentalReleases") - g = Github() - repo = g.get_repo("WebControlCNC/WebControl") - self.releases = repo.get_releases() - latestVersionGithub = 0 + print("Checking latest valid release.") + validReleases = self.getValidReleases(refresh) self.latestRelease = None - for release in self.releases: - tag_name = re.sub(r'[v]', r'', release.tag_name) - tag_float = float(tag_name) - eligible = False - if not enableExperimental: - if not self.isExperimental(tag_name): - eligible = True - else: - eligible = True - #print("tag:"+tag_name+", eligible:"+str(eligible)) - if eligible and tag_float > latestVersionGithub: - latestVersionGithub = tag_float + latestVersionNumber = 0 + latestVersionDate = datetime.datetime(1, 1, 1, 0, 0) + + for release in validReleases: + if (release.tag_version > latestVersionNumber): + latestVersionNumber = release.tag_version + latestVersionDate = release.tag_date self.latestRelease = release + elif (release.tag_version == latestVersionNumber and release.tag_date > latestVersionDate): + latestVersionDate = release.tag_date + self.latestRelease = release + + print("Latest release: " + str(self.latestRelease.tag_name)) - print("Latest pyrelease: " + str(latestVersionGithub)) - if self.latestRelease is not None and latestVersionGithub > self.data.pyInstallCurrentVersion: - print("Latest release tag: " + self.latestRelease.tag_name) + if self.latestRelease is not None and latestVersionNumber >= self.data.pyInstallCurrentVersionNumber and latestVersionDate > self.data.pyInstallCurrentVersionDate: + print("Fetching Assets for release: " + self.latestRelease.tag_name) assets = self.latestRelease.get_assets() for asset in assets: if asset.name.find(self.data.pyInstallType) != -1 and asset.name.find(self.data.pyInstallPlatform) != -1: @@ -71,73 +109,114 @@ def checkForLatestPyRelease(self): self.data.pyInstallUpdateAvailable = True self.data.pyInstallUpdateBrowserUrl = asset.browser_download_url self.data.pyInstallUpdateVersion = self.latestRelease - except Exception as e: - print("Error checking pyrelease: " + str(e)) - def isExperimental(self, tag): - ''' - Deternmines if release is experimental. All even releases are stable, odd releases are experimental - :param tag: - :return: - ''' - if float(tag) <= 0.931: # all releases before now are 'stable' - return False - lastDigit = tag[-1] - if (int(lastDigit) % 2) == 0: # only even releases are 'stable' - return False - else: - return True + except Exception as e: + print("Error checking latest valid release: " + str(e)) def processAbsolutePath(self, path): index = path.find("main.py") self.data.pyInstallInstalledPath = path[0:index - 1] print(self.data.pyInstallInstalledPath) + def wgetBarCustom(self, current, total, width=80): + percentDone = int(current / total * 100) + data = { + "percent": percentDone, + "current" : current, + "total" : total, + } + socketio.emit("message", {"command": "upgradeDownload", "data": json.dumps(data), "dataFormat": "json"}, namespace="/MaslowCNC", room="modal") + print("Downloading: %d%% [%d / %d] bytes" % (percentDone, current, total)) + def updatePyInstaller(self, bypassCheck = False): - home = self.data.config.getHome() - if self.data.pyInstallUpdateAvailable == True or bypassCheck: - if not os.path.exists(home + "/.WebControl/downloads"): - print("creating downloads directory") - os.mkdir(home + "/.WebControl/downloads") - fileList = glob.glob(home + "/.WebControl/downloads/*.gz") - for filePath in fileList: - try: + try: + home = self.data.config.getHome() + + if self.data.pyInstallUpdateAvailable == True or bypassCheck: + if not os.path.exists(home + "/.WebControl/downloads"): + self.emitStatusUpdate("Creating downloads directory.") + os.mkdir(home + "/.WebControl/downloads", exists_ok=True) + + fileList = glob.glob(home + "/.WebControl/downloads/*.gz") + + for filePath in fileList: os.remove(filePath) - except: - print("error cleaning download directory: ", filePath) - print("---") - if self.data.pyInstallPlatform == "win32" or self.data.pyInstallPlatform == "win64": - filename = wget.download(self.data.pyInstallUpdateBrowserUrl, out=home + "\\.WebControl\\downloads") - else: - filename = wget.download(self.data.pyInstallUpdateBrowserUrl, out=home + "/.WebControl/downloads") - print(filename) - if self.data.platform == "PYINSTALLER": - lhome = os.path.join(self.data.platformHome) - else: - lhome = "." - if self.data.pyInstallPlatform == "win32" or self.data.pyInstallPlatform == "win64": - path = lhome + "/tools/upgrade_webcontrol_win.bat" - copyfile(path, home + "/.WebControl/downloads/upgrade_webcontrol_win.bat") - path = lhome + "/tools/7za.exe" - copyfile(path, home + "/.WebControl/downloads/7za.exe") - self.data.pyInstallInstalledPath = self.data.pyInstallInstalledPath.replace('/', '\\') - program_name = home + "\\.WebControl\\downloads\\upgrade_webcontrol_win.bat" + self.emitStatusUpdate("Downloading new release.") + if self.data.pyInstallPlatform == "win32" or self.data.pyInstallPlatform == "win64": + filename = wget.download(self.data.pyInstallUpdateBrowserUrl, out=home + "\\.WebControl\\downloads", bar = self.wgetBarCustom) + else: + filename = wget.download(self.data.pyInstallUpdateBrowserUrl, out=home + "/.WebControl/downloads", bar = self.wgetBarCustom) + self.emitStatusUpdate("Successfully downloaded new release to:" + filename) - else: - path = lhome + "/tools/upgrade_webcontrol.sh" - copyfile(path, home + "/.WebControl/downloads/upgrade_webcontrol.sh") - program_name = home + "/.WebControl/downloads/upgrade_webcontrol.sh" - self.make_executable(home + "/.WebControl/downloads/upgrade_webcontrol.sh") - tool_path = home + "\\.WebControl\\downloads\\7za.exe" - arguments = [filename, self.data.pyInstallInstalledPath, tool_path] - command = [program_name] - command.extend(arguments) - print("popening") - print(command) - subprocess.Popen(command) - return True - return False + if self.data.platform == "PYINSTALLER": + lhome = os.path.join(self.data.platformHome) + else: + lhome = "." + + self.emitStatusUpdate("Creating target version directory.") + target_dir = self.data.pyInstallInstalledPath + '_next' + if os.path.exists(target_dir): + self.emitStatusUpdate("New release directory already exists, removing it.") + shutil.rmtree(target_dir) + os.mkdir(target_dir) + + self.emitStatusUpdate("Extracting new release.") + if self.data.pyInstallPlatform == "win32" or self.data.pyInstallPlatform == "win64": + command = [ + lhome + "/tools/7za.exe", + "x", + "-y", + filename, + "-o", + target_dir + ] + print(command) + subprocess.run(command) + else: + command = [ + "tar", + "-zxf", + filename, + "-C", + target_dir + ] + print(command) + subprocess.run(command) + + self.emitStatusUpdate("Checking for upgrade script.") + if self.data.pyInstallPlatform == "win32" or self.data.pyInstallPlatform == "win64": + upgrade_script_path = target_dir + "\\tools\\upgrade_" + self.data.pyInstallPlatform + ".bat" + else: + upgrade_script_path = target_dir + "/tools/upgrade_" + self.data.pyInstallPlatform + ".sh" + if os.path.exists(upgrade_script_path): + self.emitStatusUpdate("Running upgrade script.") + self.make_executable(upgrade_script_path) + subprocess.run([upgrade_script_path]) + else: + self.emitStatusUpdate("Upgrade script not required.") + + self.emitStatusUpdate("Backing up the current install.") + backup_path = self.data.pyInstallInstalledPath + '_old' + print("Backup location: " + backup_path) + if os.path.exists(backup_path): + self.emitStatusUpdate("Old backup found, removing it.") + shutil.rmtree(backup_path) + os.rename(self.data.pyInstallInstalledPath, self.data.pyInstallInstalledPath + '_old') + print("Backing up the current install DONE") + + self.emitStatusUpdate("Moving the target version in place.") + os.rename(target_dir, self.data.pyInstallInstalledPath) + print("Moving the target version in place DONE") + + self.emitStatusUpdate("WebControl upgrade complete.") + socketio.emit("message", {"command": "upgradeSuccess", "data": "success", "dataFormat": "text"}, namespace="/MaslowCNC", room="modal") + # Restart application terminating current. + return True + + except Exception as e: + self.emitStatusUpdate("Error updating release: " + str(e)) + return False def make_executable(self, path): print("1") @@ -148,24 +227,34 @@ def make_executable(self, path): os.chmod(path, mode) print("4") - - def update(self, version): + def update(self, releaseTagName): ''' - Need to clean this up. - :param version: + :param releaseTagName: :return: ''' - for release in self.releases: - if release.tag_name == version: - assets = release.get_assets() - for asset in assets: - if asset.name.find(self.data.pyInstallType) != -1 and asset.name.find(self.data.pyInstallPlatform) != -1: - print(asset.name) - print(asset.url) - self.data.pyInstallUpdateBrowserUrl = asset.browser_download_url - print(self.data.pyInstallUpdateBrowserUrl) - return self.updatePyInstaller(True) - print("hmmm.. issue") - return False + try: + validReleases = self.getValidReleases() + releaseAssetsFound = False + for release in validReleases: + if release.tag_name == releaseTagName: + assets = release.get_assets() + for asset in assets: + if asset.name.find(self.data.pyInstallType) != -1 and asset.name.find(self.data.pyInstallPlatform) != -1: + print(asset.name) + print(asset.url) + self.data.pyInstallUpdateBrowserUrl = asset.browser_download_url + print(self.data.pyInstallUpdateBrowserUrl) + releaseAssetsFound = True + break + if releaseAssetsFound: + return self.updatePyInstaller(True) + else: + raise Exception("Assets not found for release: " + str(releaseTagName)) + except Exception as e: + print("Update error: " + str(e)) + return False + def emitStatusUpdate(self, text): + print(text) + socketio.emit("message", {"command": "upgradeStatus", "data": text, "dataFormat": "text"}, namespace="/MaslowCNC", room="modal") diff --git a/WebPageProcessor/webPageProcessor.py b/WebPageProcessor/webPageProcessor.py index 3892cd9c..5084ff52 100644 --- a/WebPageProcessor/webPageProcessor.py +++ b/WebPageProcessor/webPageProcessor.py @@ -511,7 +511,7 @@ def createWebPage(self, pageID, isMobile, args): ) return page, "Open Board", False, "medium", "content", "footerSubmit" elif pageID == "about": - version = self.data.pyInstallCurrentVersion + version = self.data.pyInstallCurrentVersionNumber if isMobile: pageName = "about.html" else: @@ -526,30 +526,26 @@ def createWebPage(self, pageID, isMobile, args): page = render_template(pageName, pageID="gettingStarted") return page, "Getting Started", False, "medium", "content", False elif pageID == "releases": - releases = self.data.releaseManager.getReleases() - latestRelease = self.data.releaseManager.getLatestRelease() - currentRelease = "v"+str(self.data.pyInstallCurrentVersion) - for release in releases: - tag_name = re.sub(r'[v]', r'', release.tag_name) + latestRelease = self.data.releaseManager.getLatestValidRelease() + releases = self.data.releaseManager.getValidReleases() + currentRelease = "v" + str(self.data.pyInstallCurrentVersionNumber) + if isMobile: - page = render_template( - "releases_mobile.html", - title="Update Manager", - releases=releases, - latestRelease=latestRelease, - currentRelease=currentRelease, - pageID="releases", - ) + file = "releases_mobile.html" else: - page = render_template( - "releases.html", - title="Update Manager", - releases=releases, - latestRelease=latestRelease, - currentRelease=currentRelease, - pageID="releases", + file = "releases.html", + + page = render_template( + file, + title = "Update Manager", + releases = releases, + latestRelease = latestRelease, + currentRelease = currentRelease, + pageID = "releases", ) + return page, "Update Manager", False, "medium", "content", False + elif pageID == "helpPages": helpPages = self.data.helpManager.getHelpPages() if isMobile: diff --git a/main.py b/main.py index 023f9e2a..9a65a1d3 100644 --- a/main.py +++ b/main.py @@ -14,7 +14,7 @@ import time import threading import json - +from flask_socketio import join_room from flask import Flask, jsonify, render_template, current_app, request, flash, Response, send_file, send_from_directory from flask_mobility.decorators import mobile_template from werkzeug import secure_filename @@ -737,6 +737,10 @@ def log_connect(): def log_disconnect(): app.data.console_queue.put("Client disconnected") +@socketio.on("join_room", namespace="/MaslowCNC") +def joinRoom(msg): + room = msg["data"]["room"] + join_room(room) @app.template_filter('isnumber') def isnumber(s): @@ -773,7 +777,7 @@ def isnumber(s): print("opening browser") webPortStr = str(webPortInt) - webbrowser.open_new_tab("//localhost:"+webPortStr) + webbrowser.open_new_tab("http://localhost:"+webPortStr) host_name = socket.gethostname() host_ip = socket.gethostbyname(host_name) app.data.hostAddress = host_ip + ":" + webPortStr diff --git a/templates/releases.html b/templates/releases.html index c05f7147..73621600 100644 --- a/templates/releases.html +++ b/templates/releases.html @@ -1,33 +1,110 @@ {% block content %}
{{release.tag_name}}
-{{release.body|markdown}}
-{{release.tag_version}}
+ {% if release.tag_date %} +{{release.tag_date.strftime("%x")}}
+ {% endif %}{{release.body|markdown}}
+