Bundled Software Update Plugin

This commit is contained in:
Gina Häußge 2015-06-09 13:35:03 +02:00
parent cd390a53af
commit 2e6240a910
18 changed files with 1690 additions and 0 deletions

View file

@ -0,0 +1,462 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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 octoprint.plugin
import flask
import os
import threading
import time
from . import version_checks, updaters, exceptions, util
from octoprint.server.util.flask import restricted_access
from octoprint.server import admin_permission
from octoprint.util import dict_merge
import octoprint.settings
##~~ Plugin
class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
octoprint.plugin.SettingsPlugin,
octoprint.plugin.AssetPlugin,
octoprint.plugin.TemplatePlugin):
def __init__(self):
self._update_in_progress = False
self._configured_checks_mutex = threading.Lock()
self._configured_checks = None
self._version_cache = dict()
self._version_cache_ttl = 0
def initialize(self):
self._version_cache_ttl = self._settings.get_int(["cache_ttl"]) * 60
def _get_configured_checks(self):
with self._configured_checks_mutex:
if self._configured_checks is None:
self._configured_checks = self._settings.get(["checks"], merged=True)
update_check_hooks = self._plugin_manager.get_hooks("octoprint.plugin.softwareupdate.check_config")
for name, hook in update_check_hooks.items():
try:
hook_checks = hook()
except:
self._logger.exception("Error while retrieving update information from plugin {name}".format(**locals()))
else:
for key, data in hook_checks.items():
if key in self._configured_checks:
data = dict_merge(data, self._configured_checks[key])
self._configured_checks[key] = data
return self._configured_checks
#~~ SettingsPlugin API
def get_settings_defaults(self):
return {
"checks": {
"octoprint": {
"type": "github_release",
"user": "foosel",
"repo": "OctoPrint",
"update_script": "{{python}} \"{update_script}\" --python=\"{{python}}\" \"{{folder}}\" {{target}}".format(update_script=os.path.join(self._basefolder, "scripts", "update-octoprint.py")),
"restart": "octoprint"
},
},
"octoprint_restart_command": None,
"environment_restart_command": None,
"cache_ttl": 60,
}
def on_settings_save(self, data):
super(SoftwareUpdatePlugin, self).on_settings_save(data)
self._version_cache_ttl = self._settings.get_int(["cache_ttl"]) * 60
#~~ BluePrint API
@octoprint.plugin.BlueprintPlugin.route("/check", methods=["GET"])
def check_for_update(self):
if "check" in flask.request.values:
check_targets = map(str.strip, flask.request.values["check"].split(","))
else:
check_targets = None
if "force" in flask.request.values and flask.request.values["force"] in octoprint.settings.valid_boolean_trues:
force = True
else:
force=False
try:
information, update_available, update_possible = self.get_current_versions(check_targets=check_targets, force=force)
return flask.jsonify(dict(status="updatePossible" if update_available and update_possible else "updateAvailable" if update_available else "current", information=information))
except exceptions.ConfigurationInvalid as e:
flask.make_response("Update not properly configured, can't proceed: %s" % e.message, 500)
@octoprint.plugin.BlueprintPlugin.route("/update", methods=["POST"])
@restricted_access
@admin_permission.require(403)
def perform_update(self):
if self._printer.is_printing() or self._printer.is_paused():
# do not update while a print job is running
flask.make_response("Printer is currently printing or paused", 409)
if not "application/json" in flask.request.headers["Content-Type"]:
flask.make_response("Expected content-type JSON", 400)
json_data = flask.request.json
if "check" in json_data:
check_targets = map(str.strip, json_data["check"])
else:
check_targets = None
if "force" in json_data:
from octoprint.settings import valid_boolean_trues
force = (json_data["force"] in valid_boolean_trues)
else:
force = False
to_be_checked, checks = self.perform_updates(check_targets=check_targets, force=force)
return flask.jsonify(dict(order=to_be_checked, checks=checks))
#~~ Asset API
def get_assets(self):
return dict(
css=["css/softwareupdate.css"],
js=["js/softwareupdate.js"],
less=["less/softwareupdate.less"]
)
##~~ TemplatePlugin API
def get_template_configs(self):
return [
dict(type="settings", name="Software Update")
]
#~~ Updater
def get_current_versions(self, check_targets=None, force=False):
"""
Retrieves the current version information for all defined check_targets. Will retrieve information for all
available targets by default.
:param check_targets: an iterable defining the targets to check, if not supplied defaults to all targets
"""
checks = self._get_configured_checks()
if check_targets is None:
check_targets = checks.keys()
update_available = False
update_possible = False
information = dict()
for target, check in checks.items():
if not target in check_targets:
continue
try:
target_information, target_update_available, target_update_possible = self._get_current_version(target, check, force=force)
if target_information is None:
continue
except exceptions.UnknownCheckType:
self._logger.warn("Unknown update check type for %s" % target)
continue
target_information = dict_merge(dict(local=dict(name="unknown", value="unknown"), remote=dict(name="unknown", value="unknown")), target_information)
update_available = update_available or target_update_available
update_possible = update_possible or (target_update_possible and target_update_available)
information[target] = dict(updateAvailable=target_update_available, updatePossible=target_update_possible, information=target_information)
if "displayName" in check:
information[target]["displayName"] = check["displayName"]
if "displayVersion" in check:
from octoprint._version import get_versions
octoprint_version = get_versions()["version"]
local_name = target_information["local"]["name"]
local_value = target_information["local"]["value"]
information[target]["displayVersion"] = check["displayVersion"].format(octoprint_version=octoprint_version, local_name=local_name, local_value=local_value)
return information, update_available, update_possible
def _get_current_version(self, target, check, force=False):
"""
Determines the current version information for one target based on its check configuration.
"""
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():
return information, update_available, update_possible
information = dict()
update_available = False
try:
version_checker = self._get_version_checker(target, check)
information, is_current = version_checker.get_latest(target, check)
if information is not None and not is_current:
update_available = True
except exceptions.UnknownCheckType:
self._logger.warn("Unknown check type %s for %s" % (check["type"], target))
update_possible = False
except:
self._logger.exception("Could not check %s for updates" % target)
update_possible = False
else:
try:
updater = self._get_updater(target, check)
update_possible = updater.can_perform_update(target, check)
except:
update_possible = False
self._version_cache[target] = (time.time(), information, update_available, update_possible)
return information, update_available, update_possible
def _send_client_message(self, message_type, data=None):
self._plugin_manager.send_plugin_message("softwareupdate", dict(type=message_type, data=data))
def perform_updates(self, check_targets=None, force=False):
"""
Performs the updates for the given check_targets. Will update all possible targets by default.
:param check_targets: an iterable defining the targets to update, if not supplied defaults to all targets
"""
checks = self._get_configured_checks()
if check_targets is None:
check_targets = checks.keys()
to_be_updated = sorted(set(check_targets) & set(checks.keys()))
if "octoprint" in to_be_updated:
to_be_updated.remove("octoprint")
tmp = ["octoprint"] + to_be_updated
to_be_updated = tmp
updater_thread = threading.Thread(target=self._update_worker, args=(checks, to_be_updated, force))
updater_thread.daemon = False
updater_thread.start()
return to_be_updated, dict((key, check["displayName"] if "displayName" in check else key) for key, check in checks.items() if key in to_be_updated)
def _update_worker(self, checks, check_targets, force):
restart_type = None
try:
self._update_in_progress = True
target_results = dict()
error = False
### iterate over all configured targets
for target in check_targets:
if not target in checks:
continue
check = checks[target]
if "enabled" in check and not check["enabled"]:
continue
if not target in check_targets:
continue
target_error, target_result = self._perform_update(target, check, force)
error = error or target_error
if target_result is not None:
target_results[target] = target_result
if "restart" in check:
target_restart_type = check["restart"]
elif "pip" in check:
target_restart_type = "octoprint"
# if our update requires a restart we have to determine which type
if restart_type is None or (restart_type == "octoprint" and target_restart_type == "environment"):
restart_type = target_restart_type
finally:
# we might have needed to update the config, so we'll save that now
self._settings.save()
# also, we are now longer updating
self._update_in_progress = False
if error:
# if there was an unignorable error, we just return error
self._send_client_message("error", dict(results=target_results))
else:
# otherwise the update process was a success, but we might still have to restart
if restart_type is not None and restart_type in ("octoprint", "environment"):
# one of our updates requires a restart of either type "octoprint" or "environment". Let's see if
# we can actually perform that
restart_command = self._settings.get(["%s_restart_command" % restart_type])
if restart_command is not None:
self._send_client_message("restarting", dict(restart_type=restart_type, results=target_results))
try:
self._perform_restart(restart_command)
except exceptions.RestartFailed:
self._send_client_message("restart_failed", dict(restart_type=restart_type, results=target_results))
else:
# we don't have this restart type configured, we'll have to display a message that a manual
# restart is needed
self._send_client_message("restart_manually", dict(restart_type=restart_type, results=target_results))
else:
self._send_client_message("success", dict(results=target_results))
def _perform_update(self, target, check, force):
information, update_available, update_possible = self._get_current_version(target, check)
if not update_available and not force:
return False, None
if not update_possible:
self._logger.warn("Cannot perform update for %s, update type is not fully configured" % target)
return False, None
# determine the target version to update to
target_version = information["remote"]["value"]
target_error = False
### The actual update procedure starts here...
try:
self._logger.info("Starting update of %s to %s..." % (target, target_version))
self._send_client_message("updating", dict(target=target, version=target_version))
updater = self._get_updater(target, check)
if updater is None:
raise exceptions.UnknownUpdateType()
update_result = updater.perform_update(target, check, target_version)
target_result = ("success", update_result)
self._logger.info("Update of %s to %s successful!" % (target, target_version))
except exceptions.UnknownUpdateType:
self._logger.warn("Update of %s can not be performed, unknown update type" % target)
self._send_client_message("update_failed", dict(target=target, version=target_version, reason="Unknown update type"))
return False, None
except Exception as e:
self._logger.exception("Update of %s can not be performed" % target)
if not "ignorable" in check or not check["ignorable"]:
target_error = True
if isinstance(e, exceptions.UpdateError):
target_result = ("failed", e.data)
self._send_client_message("update_failed", dict(target=target, version=target_version, reason=e.data))
else:
target_result = ("failed", None)
self._send_client_message("update_failed", dict(target=target, version=target_version, reason="unknown"))
else:
# make sure that any external changes to config.yaml are loaded into the system
self._settings.load()
# persist the new version if necessary for check type
if check["type"] == "github_commit":
checks = self._settings.get(["checks"], merged=True)
if target in checks:
# TODO make this cleaner, right now it saves too much to disk
checks[target]["current"] = target_version
self._settings.set(["checks"], checks)
# we have to save here (even though that makes us save quite often) since otherwise the next
# load will overwrite our changes we just made
self._settings.save()
return target_error, target_result
def _perform_restart(self, restart_command):
"""
Performs a restart using the supplied restart_command.
"""
self._logger.info("Restarting...")
try:
util.execute(restart_command)
except exceptions.ScriptError as e:
self._logger.exception("Error while restarting")
self._logger.warn("Restart stdout:\n%s" % e.stdout)
self._logger.warn("Restart stderr:\n%s" % e.stderr)
raise exceptions.RestartFailed()
def _get_version_checker(self, target, check):
"""
Retrieves the version checker to use for given target and check configuration. Will raise an UnknownCheckType
if version checker cannot be determined.
"""
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
elif check_type == "github_commit":
return version_checks.github_commit
elif check_type == "git_commit":
return version_checks.git_commit
elif check_type == "commandline":
return version_checks.commandline
elif check_type == "python_checker":
return version_checks.python_checker
else:
raise exceptions.UnknownCheckType()
def _get_updater(self, target, check):
"""
Retrieves the updater for the given target and check configuration. Will raise an UnknownUpdateType if updater
cannot be determined.
"""
if "update_script" in check:
return updaters.update_script
elif "pip" in check:
return updaters.pip
elif "python_updater" in check:
return updaters.python_updater
else:
raise exceptions.UnknownUpdateType()
__plugin_name__ = "Software Update"
__plugin_author__ = "Gina Häußge"
__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update"
__plugin_description__ = "Allows receiving update notifications and performing updates of OctoPrint and plugins"
__plugin_license__ = "AGPLv3"
def __plugin_load__():
global __plugin_implementation__
__plugin_implementation__ = SoftwareUpdatePlugin()
global __plugin_helpers__
__plugin_helpers__ = dict(
version_checks=version_checks,
updaters=updaters,
exceptions=exceptions,
util=util
)

View file

@ -0,0 +1,37 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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"
class NoUpdateAvailable(Exception):
pass
class UpdateAlreadyInProgress(Exception):
pass
class UnknownUpdateType(Exception):
pass
class UnknownCheckType(Exception):
pass
class UpdateError(Exception):
def __init__(self, message, data):
self.message = message
self.data = data
class ScriptError(Exception):
def __init__(self, returncode, stdout, stderr):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
class RestartFailed(Exception):
pass
class ConfigurationInvalid(Exception):
pass

View file

@ -0,0 +1,180 @@
#!/bin/env python
from __future__ import absolute_import
__author__ = "Gina Haeussge <osd@foosel.net>"
__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()

View file

@ -0,0 +1 @@
td.settings_plugin_softwareupdate_column_update{width:16px}

View file

@ -0,0 +1,435 @@
$(function() {
function SoftwareUpdateViewModel(parameters) {
var self = this;
self.loginState = parameters[0];
self.printerState = parameters[1];
self.settings = parameters[2];
self.popup = undefined;
self.updateInProgress = false;
self.waitingForRestart = false;
self.restartTimeout = undefined;
self.currentlyBeingUpdated = [];
self.config_restartCommand = ko.observable();
self.config_rebootCommand = ko.observable();
self.config_cacheTtl = ko.observable();
self.configurationDialog = $("#settings_plugin_softwareupdate_configurationdialog");
self.versions = new ItemListHelper(
"plugin.softwareupdate.versions",
{
"name": function(a, b) {
// sorts ascending, puts octoprint first
if (a.key.toLocaleLowerCase() == "octoprint") return -1;
if (b.key.toLocaleLowerCase() == "octoprint") return 1;
if (a.displayName.toLocaleLowerCase() < b.displayName.toLocaleLowerCase()) return -1;
if (a.displayName.toLocaleLowerCase() > b.displayName.toLocaleLowerCase()) return 1;
return 0;
}
},
{},
"name",
[],
[],
5
);
self.onUserLoggedIn = function() {
self.performCheck();
};
self._showPopup = function(options, eventListeners) {
if (self.popup !== undefined) {
self.popup.remove();
}
self.popup = new PNotify(options);
if (eventListeners) {
var popupObj = self.popup.get();
_.each(eventListeners, function(value, key) {
popupObj.on(key, value);
})
}
};
self._updatePopup = function(options) {
if (self.popup === undefined) {
self._showPopup(options);
} else {
self.popup.update(options);
}
};
self.showPluginSettings = function() {
self._copyConfig();
self.configurationDialog.modal();
};
self.savePluginSettings = function() {
var data = {
plugins: {
softwareupdate: {
octoprint_restart_command: self.config_restartCommand(),
environment_restart_command: self.config_rebootCommand(),
cache_ttl: parseInt(self.config_cacheTtl())
}
}
};
self.settings.saveData(data, function() { self.configurationDialog.modal("hide"); self._copyConfig(); });
};
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.performCheck = function(showIfNothingNew, force, ignoreSeen) {
if (!self.loginState.isUser()) return;
var url = PLUGIN_BASEURL + "softwareupdate/check";
if (force) {
url += "?force=true";
}
$.ajax({
url: url,
type: "GET",
dataType: "json",
success: function(data) {
var versions = [];
_.each(data.information, function(value, key) {
value["key"] = key;
if (!value.hasOwnProperty("displayName") || value.displayName == "") {
value.displayName = value.key;
}
if (!value.hasOwnProperty("displayVersion") || value.displayVersion == "") {
value.displayVersion = value.information.local.name;
}
versions.push(value);
});
self.versions.updateItems(versions);
if (data.status == "updateAvailable" || data.status == "updatePossible") {
var text = gettext("There are updates available for the following components:");
text += "<ul>";
_.each(self.versions.items(), function(update_info) {
if (update_info.updateAvailable) {
var displayName = update_info.key;
if (update_info.hasOwnProperty("displayName")) {
displayName = update_info.displayName;
}
text += "<li>" + displayName + (update_info.updatePossible ? " <i class=\"icon-ok\"></i>" : "") + "</li>";
}
});
text += "</ul>";
text += "<small>" + gettext("Those components marked with <i class=\"icon-ok\"></i> can be updated directly.") + "</small>";
var options = {
title: gettext("Update Available"),
text: text,
hide: false
};
var eventListeners = {};
if (data.status == "updatePossible" && self.loginState.isAdmin()) {
// if user is admin, add action buttons
options["confirm"] = {
confirm: true,
buttons: [{
text: gettext("Ignore"),
click: function() {
self._markNotificationAsSeen(data.information);
self._showPopup({
text: gettext("You can make this message display again via \"Settings\" > \"SoftwareUpdate\" > \"Check for update now\"")
});
}
}, {
text: gettext("Update now"),
addClass: "btn-primary",
click: self.update
}]
};
options["buttons"] = {
closer: false,
sticker: false
};
}
if (ignoreSeen || !self._hasNotificationBeenSeen(data.information)) {
self._showPopup(options, eventListeners);
}
} else if (data.status == "current" && showIfNothingNew) {
self._showPopup({
title: gettext("Everything is up-to-date"),
hide: false,
type: "success"
});
}
}
});
};
self._markNotificationAsSeen = function(data) {
if (!Modernizr.localstorage)
return false;
localStorage["plugin.softwareupdate.seen_information"] = JSON.stringify(self._informationToRemoteVersions(data));
};
self._hasNotificationBeenSeen = function(data) {
if (!Modernizr.localstorage)
return false;
if (localStorage["plugin.softwareupdate.seen_information"] == undefined)
return false;
var knownData = JSON.parse(localStorage["plugin.softwareupdate.seen_information"]);
var freshData = self._informationToRemoteVersions(data);
var hasBeenSeen = true;
_.each(freshData, function(value, key) {
if (!_.has(knownData, key) || knownData[key] != freshData[key]) {
hasBeenSeen = false;
}
});
return hasBeenSeen;
};
self._informationToRemoteVersions = function(data) {
var result = {};
_.each(data, function(value, key) {
result[key] = value.information.remote.value;
});
return result;
};
self.performUpdate = function(force) {
self.updateInProgress = true;
var options = {
title: gettext("Updating..."),
text: gettext("Now updating, please wait."),
icon: "icon-cog icon-spin",
hide: false,
buttons: {
closer: false,
sticker: false
}
};
self._showPopup(options);
$.ajax({
url: PLUGIN_BASEURL + "softwareupdate/update",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
data: JSON.stringify({force: (force == true)}),
error: function() {
self.updateInProgress = false;
self._showPopup({
title: gettext("Update not started!"),
text: gettext("The update could not be started. Is it already active? Please consult the log for details."),
type: "error",
hide: false,
buttons: {
sticker: false
}
});
},
success: function(data) {
self.currentlyBeingUpdated = data.checks;
}
});
};
self.update = function(force) {
if (self.updateInProgress) return;
if (!self.loginState.isAdmin()) return;
force = (force == true);
if (self.printerState.isPrinting()) {
self._showPopup({
title: gettext("Can't update while printing"),
text: gettext("A print job is currently in progress. Updating will be prevented until it is done."),
type: "error"
});
} else {
$("#confirmation_dialog .confirmation_dialog_message").text(gettext("This will update your OctoPrint installation and restart the server."));
$("#confirmation_dialog .confirmation_dialog_acknowledge").unbind("click");
$("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {
e.preventDefault();
$("#confirmation_dialog").modal("hide");
self.performUpdate(force);
});
$("#confirmation_dialog").modal("show");
}
};
self.onServerDisconnect = function() {
if (self.restartTimeout !== undefined) {
clearTimeout(self.restartTimeout);
}
return true;
};
self.onDataUpdaterReconnect = function() {
if (self.waitingForRestart) {
self.waitingForRestart = false;
var options = {
title: gettext("Restart successful!"),
text: gettext("The server was restarted successfully. The page will now reload automatically."),
type: "success",
hide: false
};
self._showPopup(options);
self.updateInProgress = false;
var delay = 5 + Math.floor(Math.random() * 5) + 1;
setTimeout(function() {location.reload(true);}, delay * 1000);
}
};
self.onDataUpdaterPluginMessage = function(plugin, data) {
if (plugin != "softwareupdate") {
return;
}
var messageType = data.type;
var messageData = data.data;
var options = undefined;
switch (messageType) {
case "updating": {
console.log(JSON.stringify(messageData));
var name = self.currentlyBeingUpdated[messageData.target];
if (name == undefined) {
name = messageData.target;
}
self._updatePopup({
text: _.sprintf(gettext("Now updating %(name)s to %(version)s"), {name: name, version: messageData.version})
});
break;
}
case "restarting": {
console.log(JSON.stringify(messageData));
options = {
title: gettext("Update successful, restarting!"),
text: gettext("The update finished successfully and the server will now be restarted."),
type: "success",
hide: false,
buttons: {
sticker: false
}
};
self.waitingForRestart = true;
self.restartTimeout = setTimeout(function() {
self._showPopup({
title: gettext("Restart failed"),
text: gettext("The server apparently did not restart by itself, you'll have to do it manually. Please consult the log file on what went wrong."),
type: "error",
hide: false,
buttons: {
sticker: false
}
});
self.waitingForRestart = false;
}, 20000);
break;
}
case "restart_manually": {
console.log(JSON.stringify(messageData));
var restartType = messageData.restart_type;
var text = gettext("The update finished successfully, please restart OctoPrint now.");
if (restartType == "environment") {
text = gettext("The update finished successfully, please reboot the server now.");
}
options = {
title: gettext("Update successful, restart required!"),
text: text,
type: "success",
hide: false,
buttons: {
sticker: false
}
};
self.updateInProgress = false;
break;
}
case "restart_failed": {
var restartType = messageData.restart_type;
var text = gettext("Restarting OctoPrint failed, please restart it manually. You might also want to consult the log file on what went wrong here.");
if (restartType == "environment") {
text = gettext("Rebooting the server failed, please reboot it manually. You might also want to consult the log file on what went wrong here.");
}
options = {
title: gettext("Restart failed"),
test: gettext("The server apparently did not restart by itself, you'll have to do it manually. Please consult the log file on what went wrong."),
type: "error",
hide: false,
buttons: {
sticker: false
}
};
self.waitingForRestart = false;
self.updateInProgress = false;
break;
}
case "success": {
options = {
title: gettext("Update successful!"),
text: gettext("The update finished successfully."),
type: "success",
hide: false,
buttons: {
sticker: false
}
};
self.updateInProgress = false;
break;
}
case "error": {
self._showPopup({
title: gettext("Update failed!"),
text: gettext("The update did not finish successfully. Please consult the log for details."),
type: "error",
hide: false,
buttons: {
sticker: false
}
});
self.updateInProgress = false;
break;
}
}
if (options != undefined) {
self._showPopup(options);
}
};
}
// view model class, parameters for constructor, container to bind to
ADDITIONAL_VIEWMODELS.push([SoftwareUpdateViewModel, ["loginStateViewModel", "printerStateViewModel", "settingsViewModel"], document.getElementById("settings_plugin_softwareupdate")]);
});

View file

@ -0,0 +1,3 @@
td.settings_plugin_softwareupdate_column_update {
width: 16px;
}

View file

@ -0,0 +1,86 @@
<div class="pull-right">
<button class="btn btn-small" data-bind="click: function() { $root.showPluginSettings(); }" title="{{ _('Plugin Configuration') }}"><i class="icon-wrench"></i></button>
</div>
<h3>{{ _('Current versions') }}</h3>
<table class="table table-striped table-hover table-condensed table-hover">
<tbody data-bind="foreach: versions.paginatedItems">
<tr data-bind="attr: {title: displayName}">
<td class="settings_plugin_softwareupdate_column_update">
<span data-bind="invisible: !updateAvailable"><i class="icon-bell" title="{{ _('Update available') }}"></i></span>
</td>
<td class="settings_plugin_softwareupdate_column_information">
<strong data-bind="text: displayName"></strong>: <span data-bind="text: displayVersion"></span><br>
<small class="muted">
{{ _('Local:') }} <span data-bind="text: information.local.name"></span><br>
{{ _('Remote:') }} <span data-bind="text: information.remote.name"></span>
</small>
</td>
</tr>
</tbody>
</table>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: versions.currentPage() === 0}">
<a href="#" data-bind="click: versions.prevPage">«</a>
</li>
</ul>
<ul data-bind="foreach: versions.pages">
<li data-bind="css: { active: $data.number === $root.versions.currentPage(), disabled: $data.number === -1 }">
<a href="#" data-bind="text: $data.text, click: function() { $root.versions.changePage($data.number); }"></a>
</li>
</ul>
<ul>
<li data-bind="css: {disabled: versions.currentPage() === versions.lastPage()}">
<a href="#" data-bind="click: versions.nextPage">»</a>
</li>
</ul>
</div>
<button class="btn btn-primary btn-block" data-bind="click: function() { $root.performCheck(true, false, true); }">{{ _('Check for update now') }}</button>
<div>
<div><small><a href="#" class="muted" onclick="$(this).children().toggleClass('icon-caret-right icon-caret-down').parent().parent().parent().next().slideToggle('fast')"><i class="icon-caret-right"></i> {{ _('Advanced options') }}</a></small></div>
<div class="hide">
<button class="btn btn-block" data-bind="click: function() { $root.performCheck(true, true, true); }">{{ _('Force check for update (overrides cache used for update checks)') }}</button>
<button class="btn btn-block btn-danger" data-bind="click: function() { $root.update(true); }">{{ _('Force update now (even if no new versions are available)') }}</button>
</div>
</div>
<div id="settings_plugin_softwareupdate_configurationdialog" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
<h3>{{ _('Plugin Configuration') }}</h3>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label">{{ _('Restart Command') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: config_restartCommand">
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Reboot Command') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: config_rebootCommand">
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Version cache TTL') }}</label>
<div class="controls">
<div class="input-append">
<input type="number" class="input-mini" data-bind="value: config_cacheTtl">
<span class="add-on">min</span>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Cancel') }}</button>
<button class="btn btn-primary" data-bind="click: savePluginSettings" aria-hidden="true">{{ _('Save') }}</button>
</div>
</div>

View file

@ -0,0 +1,8 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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"
from . import pip, python_updater, update_script

View file

@ -0,0 +1,37 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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 logging
try:
import pip as _pip
except:
_pip = None
def can_perform_update(target, check):
return "pip" in check and _pip is not None
def perform_update(target, check, target_version):
logger = logging.getLogger("octoprint.plugins.softwareupdate.updaters.pip")
install_arg = check["pip"].format(target_version=target_version)
logger.debug("Target: %s, executing pip install %s" % (target, install_arg))
pip_args = ["install", check["pip"].format(target_version=target_version)]
_pip.main(pip_args)
if "force_reinstall" in check and check["force_reinstall"]:
# if force_reinstall is true, we need to install the package a second time, this time forcing its reinstall
# without forcing its dependencies too
logger.debug("Target. %s, executing pip install %s --ignore-reinstalled --force-reinstall --no-deps" % (target, install_arg))
pip_args += ["--ignore-installed", "--force-reinstall", "--no-deps"]
_pip.main(pip_args)
return "ok"

View file

@ -0,0 +1,14 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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"
def can_perform_update(target, check):
return "python_updater" in check and check["python_updater"] is not None
def perform_update(target, check, target_version):
return check["python_updater"].perform_update(target, check, target_version)

View file

@ -0,0 +1,79 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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 sys
import logging
from ..exceptions import ScriptError, ConfigurationInvalid, UpdateError
from ..util import execute
def can_perform_update(target, check):
return "update_script" in check and ("checkout_folder" in check or "update_folder" in check)
def perform_update(target, check, target_version):
logger = logging.getLogger("octoprint.plugins.softwareupdate.updaters.update_script")
if not can_perform_update(target, check):
raise ConfigurationInvalid("checkout_folder and update_folder are missing for update target %s, one is needed" % target)
update_script = check["update_script"]
folder = check["update_folder"] if "update_folder" in check else check["checkout_folder"]
pre_update_script = check["pre_update_script"] if "pre_update_script" in check else None
post_update_script = check["post_update_script"] if "post_update_script" in check else None
update_stdout = ""
update_stderr = ""
### pre update
if pre_update_script is not None:
logger.debug("Target: %s, running pre-update script: %s" % (target, pre_update_script))
try:
returncode, stdout, stderr = execute(pre_update_script, cwd=folder)
update_stdout += stdout
update_stderr += stderr
except ScriptError as e:
logger.exception("Target: %s, error while executing pre update script, got returncode %r" % (target, e.returncode))
logger.warn("Target: %s, pre-update stdout:\n%s" % (target, e.stdout))
logger.warn("Target: %s, pre-update stderr:\n%s" % (target, e.stderr))
### update
try:
update_command = update_script.format(python=sys.executable, folder=folder, target=target_version)
logger.debug("Target %s, running update script: %s" % (target, update_command))
returncode, stdout, stderr = execute(update_command, cwd=folder)
update_stdout += stdout
update_stderr += stderr
except ScriptError as e:
logger.exception("Target: %s, error while executing update script, got returncode %r" % (target, e.returncode))
logger.warn("Target: %s, update stdout:\n%s" % (target, e.stdout))
logger.warn("Target: %s, update stderr:\n%s" % (target, e.stderr))
raise UpdateError("Error while executing update script for %s", (e.stdout, e.stderr))
### post update
if post_update_script is not None:
logger.debug("Target: %s, running post-update script %s..." % (target, post_update_script))
try:
returncode, stdout, stderr = execute(post_update_script, cwd=folder)
update_stdout += stdout
update_stderr += stderr
except ScriptError as e:
logger.exception("Target: %s, error while executing post update script, got returncode %r" % (target, e.returncode))
logger.warn("Target: %s, post-update stdout:\n%s" % (target, e.stdout))
logger.warn("Target: %s, post-update stderr:\n%s" % (target, e.stderr))
logger.debug("Target: %s, update stdout:\n%s" % (target, update_stdout))
logger.debug("Target: %s, update stderr:\n%s" % (target, update_stderr))
### result
return update_stdout, update_stderr

View file

@ -0,0 +1,27 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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"
from .exceptions import ScriptError
def execute(command, cwd=None, evaluate_returncode=True):
import sarge
p = None
try:
p = sarge.run(command, cwd=cwd, stdout=sarge.Capture(), stderr=sarge.Capture())
except:
returncode = p.returncode if p is not None else None
stdout = p.stdout.text if p is not None and p.stdout is not None else ""
stderr = p.stderr.text if p is not None and p.stderr is not None else ""
raise ScriptError(returncode, stdout, stderr)
if evaluate_returncode and p.returncode != 0:
raise ScriptError(p.returncode, p.stdout.text, p.stderr.text)
return p.returncode, p.stdout.text, p.stderr.text

View file

@ -0,0 +1,20 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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"
from . import commandline, git_commit, github_commit, github_release, python_checker
def log_github_ratelimit(logger, r):
ratelimit = r.headers["X-RateLimit-Limit"] if "X-RateLimit-Limit" in r.headers else "?"
remaining = r.headers["X-RateLimit-Remaining"] if "X-RateLimit-Remaining" in r.headers else "?"
reset = r.headers["X-RateLimit-Reset"] if "X-RateLimit-Reset" in r.headers else None
try:
import time
reset = time.strftime("%Y-%m-%d %H:%M", time.gmtime(int(reset)))
except:
reset = "?"
logger.debug("Github rate limit: %s/%s, reset at %s" % (remaining, ratelimit, reset))

View file

@ -0,0 +1,52 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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 logging
from ..exceptions import ConfigurationInvalid
from ..util import execute
def get_latest(target, check):
if not "command" in check:
raise ConfigurationInvalid("Update configuration for %s of type commandline needs command defined" % target)
returncode, stdout, stderr = execute(check["command"], evaluate_returncode=False)
# We expect command line check commands to
#
# * have a return code of 0 if an update is available, a value != 0 otherwise
# * return the display name of the new version as the final line on stdout
# * return the display name of the current version as the next to final line on stdout
#
# Example output:
# 1.1.0
# 1.1.1
#
# 1.1.0 is the current version, 1.1.1 is the remote version. If only one line is output, it's taken to be the
# display name of the new version
stdout_lines = filter(lambda x: len(x.strip()), stdout.splitlines())
local_name = stdout_lines[-2] if len(stdout_lines) >= 2 else "unknown"
remote_name = stdout_lines[-1] if len(stdout_lines) >= 1 else "unknown"
is_current = returncode != 0
information =dict(
local=dict(
name=local_name,
value=local_name,
),
remote=dict(
name=remote_name,
value=remote_name
)
)
logger = logging.getLogger("octoprint.plugins.softwareupdate.version_checks.github_commit")
logger.debug("Target: %s, local: %s, remote: %s" % (target, local_name, remote_name))
return information, is_current

View file

@ -0,0 +1,88 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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
import logging
from ..exceptions import ConfigurationInvalid
def _get_git_executables():
GITS = ["git"]
if sys.platform == "win32":
GITS = ["git.cmd", "git.exe"]
return GITS
def _git(args, cwd, hide_stderr=False):
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
return None, None
else:
return None, None
stdout = p.communicate()[0].strip()
if sys.version >= '3':
stdout = stdout.decode()
if p.returncode != 0:
return p.returncode, None
return p.returncode, stdout
def get_latest(target, check):
if not "checkout_folder" in check:
raise ConfigurationInvalid("Update configuration for %s needs checkout_folder" % target)
checkout_folder = check["checkout_folder"]
returncode, _ = _git(["fetch"], checkout_folder)
if returncode != 0:
return None, True
returncode, local_commit = _git(["rev-parse", "@{0}"], checkout_folder)
if returncode != 0:
return None, True
returncode, remote_commit = _git(["rev-parse", "@{u}"], checkout_folder)
if returncode != 0:
return None, True
return_code, base = _git(["merge-base", "@{0}", "@{u}"], checkout_folder)
if returncode != 0:
return None, True
if local_commit == remote_commit or remote_commit == base:
information = dict(
local=dict(name="Commit %s" % local_commit, value=local_commit),
remote=dict(name="Commit %s" % local_commit, value=local_commit)
)
is_current = True
else:
information = dict(
local=dict(name="Commit %s" % local_commit, value=local_commit),
remote=dict(name="Commit %s" % remote_commit, value=remote_commit)
)
is_current = local_commit == remote_commit
logger = logging.getLogger("octoprint.plugins.softwareupdate.version_checks.git_commit")
logger.debug("Target: %s, local: %s, remote: %s" % (target, local_commit, remote_commit))
return information, is_current

View file

@ -0,0 +1,56 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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 requests
import logging
from ..exceptions import ConfigurationInvalid
BRANCH_HEAD_URL = "https://api.github.com/repos/{user}/{repo}/git/refs/heads/{branch}"
logger = logging.getLogger("octoprint.plugins.softwareupdate.version_checks.github_commit")
def _get_latest_commit(user, repo, branch):
r = requests.get(BRANCH_HEAD_URL.format(user=user, repo=repo, branch=branch))
from . import log_github_ratelimit
log_github_ratelimit(logger, r)
if not r.status_code == requests.codes.ok:
return None
reference = r.json()
if not "object" in reference or not "sha" in reference["object"]:
return None
return reference["object"]["sha"]
def get_latest(target, check):
if "user" not in check or "repo" not in check:
raise ConfigurationInvalid("Update configuration for %s of type github_commit needs all of user and repo" % target)
branch = "master"
if "branch" in check:
branch = check["branch"]
current = None
if "current" in check:
current = check["current"]
remote_commit = _get_latest_commit(check["user"], check["repo"], branch)
information = dict(
local=dict(name="Commit {commit}".format(commit=current if current is not None else "unknown"), value=current),
remote=dict(name="Commit {commit}".format(commit=remote_commit if remote_commit is not None else "unknown"), value=remote_commit)
)
is_current = (current is not None and current == remote_commit) or remote_commit is None
logger.debug("Target: %s, local: %s, remote: %s" % (target, current, remote_commit))
return information, is_current

View file

@ -0,0 +1,91 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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 requests
import logging
from ..exceptions import ConfigurationInvalid
RELEASE_URL = "https://api.github.com/repos/{user}/{repo}/releases"
logger = logging.getLogger("octoprint.plugins.softwareupdate.version_checks.github_release")
def _get_latest_release(user, repo, include_prerelease=False):
r = requests.get(RELEASE_URL.format(user=user, repo=repo))
from . import log_github_ratelimit
log_github_ratelimit(logger, r)
if not r.status_code == requests.codes.ok:
return None, None
releases = r.json()
# filter out prereleases and drafts
if include_prerelease:
releases = filter(lambda rel: not rel["draft"], releases)
else:
releases = filter(lambda rel: not rel["prerelease"] and not rel["draft"], releases)
if not releases:
return None, None
# sort by date
comp = lambda a, b: cmp(a["published_at"], b["published_at"])
releases = sorted(releases, cmp=comp)
# latest release = last in list
latest = releases[-1]
return latest["name"], latest["tag_name"]
def _is_current(release_information, compare_type, custom=None):
if release_information["remote"]["value"] is None:
return True
if not compare_type in ("semantic", "unequal", "custom") or compare_type == "custom" and custom is None:
compare_type = "semantic"
try:
if compare_type == "semantic":
import semantic_version
local_version = semantic_version.Version(release_information["local"]["value"])
remote_version = semantic_version.Version(release_information["remote"]["value"])
return local_version >= remote_version
elif compare_type == "custom":
return custom(release_information["local"], release_information["remote"])
else:
return release_information["local"]["value"] == release_information["remote"]["value"]
except:
logger.exception("Could not check if version is current due to an error, assuming it is")
return True
def get_latest(target, check, custom_compare=None):
if not "user" in check or not "repo" in check:
raise ConfigurationInvalid("github_release update configuration for %s needs user and repo set" % target)
current = None
if "current" in check:
current = check["current"]
remote_name, remote_tag = _get_latest_release(check["user"], check["repo"], include_prerelease=check["prerelease"] == True if "prerelease" in check else False)
compare_type = check["release_compare"] if "release_compare" in check else "semantic"
information =dict(
local=dict(name=current, value=current),
remote=dict(name=remote_name, value=remote_tag)
)
logger.debug("Target: %s, local: %s, remote: %s" % (target, current, remote_tag))
return information, _is_current(information, compare_type, custom=custom_compare)

View file

@ -0,0 +1,14 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__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"
from ..exceptions import ConfigurationInvalid
def get_latest(target, check, full_data=False):
if not "python_checker" in check:
raise ConfigurationInvalid("Update configuration for %s of type commandline needs command defined" % target)
return check["python_checker"].get_latest(target, check, full_data=full_data)