diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index 35eb45e7..3932db8c 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -12,6 +12,7 @@ import flask import os import threading import time +import hashlib from . import version_checks, updaters, exceptions, util @@ -83,8 +84,12 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, self._logger.exception("Error while loading version cache from disk") else: try: - if "octoprint" in data and len(data["octoprint"]) == 4 and "local" in data["octoprint"][1] and "value" in data["octoprint"][1]["local"]: - data_version = data["octoprint"][1]["local"]["value"] + if not isinstance(data, dict): + self._logger.info("Version cache was created in a different format, not using it") + return + + if "__version" in data: + data_version = data["__version"] else: self._logger.info("Can't determine version of OctoPrint version cache was created for, not using it") return @@ -102,24 +107,18 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, self._logger.exception("Error parsing in version cache data") def _save_version_cache(self): - import tempfile import yaml - import shutil + from octoprint.util import atomic_write + from octoprint._version import get_versions - file_obj = tempfile.NamedTemporaryFile(delete=False) - try: + octoprint_version = get_versions()["version"] + self._version_cache["__version"] = octoprint_version + + with atomic_write(self._version_cache_path) as file_obj: yaml.safe_dump(self._version_cache, stream=file_obj, default_flow_style=False, indent=" ", allow_unicode=True) - file_obj.close() - shutil.move(file_obj.name, self._version_cache_path) - self._version_cache_dirty = False - self._logger.info("Saved version cache to disk") - finally: - try: - if os.path.exists(file_obj.name): - os.remove(file_obj.name) - except Exception as e: - self._logger.warn("Could not delete file {}: {}".format(file_obj.name, str(e))) + self._version_cache_dirty = False + self._logger.info("Saved version cache to disk") #~~ SettingsPlugin API @@ -127,61 +126,113 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, return { "checks": { "octoprint": { - "update_folder": "/home/pi/OctoPrint", + "checkout_folder": "/home/pi/OctoPrint", "type": "github_commit", "repo": "OctoPrint", "user": "mrbeam", "branch": "stable-1.2.2", "update_script": "{{python}} \"{update_script}\" --python=\"{{python}}\" \"{{folder}}\" {{target}}".format(update_script=os.path.join(self._basefolder, "scripts", "update-octoprint.py")), - "restart": "octoprint" + "restart": "octoprint", + "current": "Unknown" }, "svgtogcode": { - "update_folder": "/home/pi/mrbeam-inkscape-ext", + "checkout_folder": "/home/pi/mrbeam-inkscape-ext", "type": "github_commit", "repo": "mrbeam-inkscape-ext", "user": "mrbeam", - "branch": "mrbeam-stable", + "branch": "stable-1.2.2", + "update_script": "{{python}} \"{update_script}\" \"{{folder}}\" {{target}}".format(update_script=os.path.join(self._basefolder, "scripts", "git-pull.py")), + "restart": "octoprint", + "current": "Unknown" }, "lcd": { - "update_folder": "/home/pi/lcd", + "checkout_folder": "/home/pi/lcd", "type": "github_commit", "repo": "lcd", "user": "mrbeam", - "branch": "mrbeam-stable", + "branch": "stable-1.2.2", + "update_script": "{{python}} \"{update_script}\" \"{{folder}}\" {{target}}".format(update_script=os.path.join(self._basefolder, "scripts", "git-pull.py")), + "restart": "environment", + "current": "Unknown" }, "netconnectd": { - "update_folder": "/home/pi/netconnectd", + "checkout_folder": "/home/pi/netconnectd", "type": "github_commit", "repo": "netconnectd", "user": "mrbeam", - "branch": "mrbeam-stable", + "branch": "stable-1.2.2", + "update_script": "{{python}} \"{update_script}\" \"{{folder}}\" {{target}}".format(update_script=os.path.join(self._basefolder, "scripts", "git-pull.py")), + "restart": "environment", + "current": "Unknown" }, }, "octoprint_restart_command": "sudo service octoprint restart", "environment_restart_command": "sudo shutdown -r now", "pip_command": None, - "cache_ttl": 60, + "cache_ttl": 12 * 60, } def on_settings_load(self): data = dict(octoprint.plugin.SettingsPlugin.on_settings_load(self)) if "checks" in data: del data["checks"] + + checks = self._get_configured_checks() + if "octoprint" in checks: + if "checkout_folder" in checks["octoprint"]: + data["octoprint_checkout_folder"] = checks["octoprint"]["checkout_folder"] + elif "update_folder" in checks["octoprint"]: + data["octoprint_checkout_folder"] = checks["octoprint"]["update_folder"] + else: + data["octoprint_checkout_folder"] = None + data["octoprint_type"] = checks["octoprint"].get("type", None) + else: + data["octoprint_checkout_folder"] = None + data["octoprint_type"] = None + return data def on_settings_save(self, data): for key in self.get_settings_defaults(): - if key == "checks" or key == "cache_ttl": + if key == "checks" or key == "cache_ttl" or key == "octoprint_checkout_folder" or key == "octoprint_type": continue if key in data: self._settings.set([key], data[key]) if "cache_ttl" in data: self._settings.set_int(["cache_ttl"], data["cache_ttl"]) - self._version_cache_ttl = self._settings.get_int(["cache_ttl"]) * 60 + checks = self._get_configured_checks() + if "octoprint" in checks: + check = checks["octoprint"] + update_type = check.get("type", None) + checkout_folder = check.get("checkout_folder", None) + update_folder = check.get("update_folder", None) + + defaults = dict( + plugins=dict(softwareupdate=dict( + checks=dict( + octoprint=dict( + type=update_type, + checkout_folder=checkout_folder, + update_folder=update_folder + ) + ) + )) + ) + + if "octoprint_checkout_folder" in data: + self._settings.set(["checks", "octoprint", "checkout_folder"], data["octoprint_checkout_folder"], defaults=defaults, force=True) + if update_folder and data["octoprint_checkout_folder"]: + self._settings.set(["checks", "octoprint", "update_folder"], None, defaults=defaults, force=True) + self._refresh_configured_checks = True + + if "octoprint_type" in data and data["octoprint_type"] in ("github_release", "git_commit"): + self._settings.set(["checks", "octoprint", "type"], data["octoprint_type"], defaults=defaults, force=True) + self._refresh_configured_checks = True + def get_settings_version(self): return 3 @@ -363,7 +414,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, try: target_information, target_update_available, target_update_possible = self._get_current_version(target, populated_check, force=force) if target_information is None: - continue + target_information = dict() except exceptions.UnknownCheckType: self._logger.warn("Unknown update check type for %s" % target) continue @@ -382,22 +433,29 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, updatePossible=target_update_possible, information=target_information, displayName=populated_check["displayName"], - displayVersion=populated_check["displayVersion"].format(octoprint_version=octoprint_version, local_name=local_name, local_value=local_value)) + displayVersion=populated_check["displayVersion"].format(octoprint_version=octoprint_version, local_name=local_name, local_value=local_value), + check=populated_check) if self._version_cache_dirty: self._save_version_cache() return information, update_available, update_possible + def _get_check_hash(self, check): + hash = hashlib.md5() + hash.update(repr(check)) + return hash.hexdigest() + def _get_current_version(self, target, check, force=False): """ Determines the current version information for one target based on its check configuration. """ + current_hash = self._get_check_hash(check) if target in self._version_cache and not force: - timestamp, information, update_available, update_possible = self._version_cache[target] - if timestamp + self._version_cache_ttl >= time.time() > timestamp: + data = self._version_cache[target] + if data["hash"] == current_hash and data["timestamp"] + self._version_cache_ttl >= time.time() > data["timestamp"]: # we also check that timestamp < now to not get confused too much by clock changes - return information, update_available, update_possible + return data["information"], data["available"], data["possible"] information = dict() update_available = False @@ -421,7 +479,11 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, except: update_possible = False - self._version_cache[target] = (time.time(), information, update_available, update_possible) + self._version_cache[target] = dict(timestamp=time.time(), + hash=current_hash, + information=information, + available=update_available, + possible=update_possible) self._version_cache_dirty = True return information, update_available, update_possible @@ -631,13 +693,6 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if not "type" in check: raise exceptions.ConfigurationInvalid("no check type defined") - if target == "octoprint": - from octoprint._version import get_versions - from flask.ext.babel import gettext - check["displayName"] = gettext("OctoPrint") - check["displayVersion"] = "{octoprint_version}" - check["current"] = get_versions()["version"] - check_type = check["type"] if check_type == "github_release": return version_checks.github_release diff --git a/src/octoprint/plugins/softwareupdate/scripts/git-pull.py b/src/octoprint/plugins/softwareupdate/scripts/git-pull.py new file mode 100644 index 00000000..a0688e4f --- /dev/null +++ b/src/octoprint/plugins/softwareupdate/scripts/git-pull.py @@ -0,0 +1,180 @@ +#!/bin/env python +from __future__ import absolute_import + +__author__ = "Gina Haeussge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" + + +import errno +import subprocess +import sys + + +def _get_git_executables(): + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + return GITS + + +def _git(args, cwd, hide_stderr=False, verbose=False, git_executable=None): + if git_executable is not None: + commands = [git_executable] + else: + commands = _get_git_executables() + + for c in commands: + try: + p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % args[0]) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None, None + + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % args[0]) + + return p.returncode, stdout + + +def _python(args, cwd, python_executable, sudo=False): + command = [python_executable] + args + if sudo: + command = ["sudo"] + command + try: + p = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + except: + return None, None + + stdout = p.communicate()[0].strip() + if sys.version >= "3": + stdout = stdout.decode() + + return p.returncode, stdout + + +def update_source(git_executable, folder, target, force=False): + print(">>> Running: git diff --shortstat") + returncode, stdout = _git(["diff", "--shortstat"], folder, git_executable=git_executable) + if returncode != 0: + raise RuntimeError("Could not update, \"git diff\" failed with returncode %d: %s" % (returncode, stdout)) + if stdout and stdout.strip(): + # we got changes in the working tree, maybe from the user, so we'll now rescue those into a patch + import time + import os + timestamp = time.strftime("%Y%m%d%H%M") + patch = os.path.join(folder, "%s-preupdate.patch" % timestamp) + + print(">>> Running: git diff and saving output to %s" % timestamp) + returncode, stdout = _git(["diff"], folder, git_executable=git_executable) + if returncode != 0: + raise RuntimeError("Could not update, installation directory was dirty and state could not be persisted as a patch to %s" % patch) + + with open(patch, "wb") as f: + f.write(stdout) + + print(">>> Running: git reset --hard") + returncode, stdout = _git(["reset", "--hard"], folder, git_executable=git_executable) + if returncode != 0: + raise RuntimeError("Could not update, \"git reset --hard\" failed with returncode %d: %s" % (returncode, stdout)) + + print(">>> Running: git pull") + returncode, stdout = _git(["pull"], folder, git_executable=git_executable) + if returncode != 0: + raise RuntimeError("Could not update, \"git pull\" failed with returncode %d: %s" % (returncode, stdout)) + print(stdout) + + if force: + reset_command = ["reset"] + reset_command += [target] + + print(">>> Running: git %s" % " ".join(reset_command)) + returncode, stdout = _git(reset_command, folder, git_executable=git_executable) + if returncode != 0: + raise RuntimeError("Error while updating, \"git %s\" failed with returncode %d: %s" % (" ".join(reset_command), returncode, stdout)) + print(stdout) + + +def install_source(python_executable, folder, user=False, sudo=False): + print(">>> Running: python setup.py clean") + returncode, stdout = _python(["setup.py", "clean"], folder, python_executable) + if returncode != 0: + print("\"python setup.py clean\" failed with returncode %d: %s" % (returncode, stdout)) + print("Continuing anyways") + print(stdout) + + print(">>> Running: python setup.py install") + args = ["setup.py", "install"] + if user: + args.append("--user") + returncode, stdout = _python(args, folder, python_executable, sudo=sudo) + if returncode != 0: + raise RuntimeError("Could not update, \"python setup.py install\" failed with returncode %d: %s" % (returncode, stdout)) + print(stdout) + + +def parse_arguments(): + import argparse + + parser = argparse.ArgumentParser(prog="update-octoprint.py") + + parser.add_argument("--git", action="store", type=str, dest="git_executable", + help="Specify git executable to use") + parser.add_argument("--python", action="store", type=str, dest="python_executable", + help="Specify python executable to use") + parser.add_argument("--force", action="store_true", dest="force", + help="Set this to force the update to only the specified version (nothing newer)") + parser.add_argument("--sudo", action="store_true", dest="sudo", + help="Install with sudo") + parser.add_argument("--user", action="store_true", dest="user", + help="Install to the user site directory instead of the general site directory") + parser.add_argument("folder", type=str, + help="Specify the base folder of the OctoPrint installation to update") + parser.add_argument("target", type=str, + help="Specify the commit or tag to which to update") + + args = parser.parse_args() + + return args + +def main(): + args = parse_arguments() + + git_executable = None + if args.git_executable: + git_executable = args.git_executable + + python_executable = sys.executable + if args.python_executable: + python_executable = args.python_executable + + folder = args.folder + target = args.target + + import os + if not os.access(folder, os.W_OK): + raise RuntimeError("Could not update, base folder is not writable") + + update_source(git_executable, folder, target, force=args.force) + #install_source(python_executable, folder, user=args.user, sudo=args.sudo) + +if __name__ == "__main__": + main() diff --git a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js index 19c88137..2da08896 100644 --- a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js +++ b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js @@ -13,12 +13,20 @@ $(function() { self.currentlyBeingUpdated = []; - self.config_restartCommand = ko.observable(); - self.config_rebootCommand = ko.observable(); + self.octoprintUnconfigured = ko.observable(); + self.octoprintUnreleased = ko.observable(); + self.config_cacheTtl = ko.observable(); + self.config_checkoutFolder = ko.observable(); + self.config_checkType = ko.observable(); self.configurationDialog = $("#settings_plugin_softwareupdate_configurationdialog"); + self.config_availableCheckTypes = [ + {"key": "github_release", "name": gettext("Release")}, + {"key": "git_commit", "name": gettext("Commit")} + ]; + self.versions = new ItemListHelper( "plugin.softwareupdate.versions", { @@ -78,19 +86,23 @@ $(function() { var data = { plugins: { softwareupdate: { - octoprint_restart_command: self.config_restartCommand(), - environment_restart_command: self.config_rebootCommand(), - cache_ttl: parseInt(self.config_cacheTtl()) + cache_ttl: parseInt(self.config_cacheTtl()), + octoprint_checkout_folder: self.config_checkoutFolder(), + octoprint_type: self.config_checkType() } } }; - self.settings.saveData(data, function() { self.configurationDialog.modal("hide"); self._copyConfig(); }); + self.settings.saveData(data, function() { + self.configurationDialog.modal("hide"); + self._copyConfig(); + self.performCheck(); + }); }; self._copyConfig = function() { - self.config_restartCommand(self.settings.settings.plugins.softwareupdate.octoprint_restart_command()); - self.config_rebootCommand(self.settings.settings.plugins.softwareupdate.environment_restart_command()); self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl()); + self.config_checkoutFolder(self.settings.settings.plugins.softwareupdate.octoprint_checkout_folder()); + self.config_checkType(self.settings.settings.plugins.softwareupdate.octoprint_type()); }; self.fromCheckResponse = function(data, ignoreSeen, showIfNothingNew) { @@ -109,6 +121,25 @@ $(function() { }); self.versions.updateItems(versions); + var octoprint = data.information["octoprint"]; + if (octoprint && octoprint.hasOwnProperty("check")) { + var check = octoprint.check; + if (BRANCH != "master" && check["type"] == "github_release") { + self.octoprintUnreleased(true); + } else { + self.octoprintUnreleased(false); + } + + var checkoutFolder = (check["checkout_folder"] || "").trim(); + var updateFolder = (check["update_folder"] || "").trim(); + var checkType = check["type"] || ""; + if ((checkType == "github_release" || checkType == "git_commit") && checkoutFolder == "" && updateFolder == "") { + self.octoprintUnconfigured(true); + } else { + self.octoprintUnconfigured(false); + } + } + if (data.status == "updateAvailable" || data.status == "updatePossible") { var text = gettext("There are updates available for the following components:"); diff --git a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 index 96f01713..c3b0bdc8 100644 --- a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 +++ b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 @@ -1,3 +1,21 @@ +
{% trans %} + Please configure the checkout folder of OctoPrint, otherwise + this plugin won't be able to update it. Click on the button + to do this. Also refer to the Documentation. +{% endtrans %}
+
{% trans %} +

+ You are running a non-release version of OctoPrint but are tracking OctoPrint + releases. +

+ You probably want OctoPrint to track the matching development version instead. + If you have a local OctoPrint checkout folder switched to another branch, + simply switching over to "Commit" tracking will already + take care of that. Otherwise please take a look at the + Documentation. +

+{% endtrans %}
+
@@ -11,7 +29,7 @@ - :
+ :
{{ _('Installed:') }}
{{ _('Available:') }} @@ -44,7 +62,7 @@
{{ _('Advanced options') }}
- +
@@ -56,15 +74,15 @@