SWU: Various improvements for better usability
* Allow configuration of checkout folder and version tracking type
via Plugin Configuration
* Display message to use if checkout folder is not configured or a
non-release version is running and version tracking against releases
is enabled
* Clear version cache when a change in the check configuration is
detected
* Mark check configurations for which an update is not possible with
a little exclamation mark
This commit is contained in:
parent
cc8dcef8a1
commit
8666a28f64
4 changed files with 158 additions and 29 deletions
|
|
@ -14,6 +14,7 @@ import threading
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from . import version_checks, updaters, exceptions, util
|
from . import version_checks, updaters, exceptions, util
|
||||||
|
|
||||||
|
|
@ -99,8 +100,12 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
||||||
self._logger.exception("Error while loading version cache from disk")
|
self._logger.exception("Error while loading version cache from disk")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if "octoprint" in data and len(data["octoprint"]) == 4 and "local" in data["octoprint"][1] and "value" in data["octoprint"][1]["local"]:
|
if not isinstance(data, dict):
|
||||||
data_version = data["octoprint"][1]["local"]["value"]
|
self._logger.info("Version cache was created in a different format, not using it")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "__version" in data:
|
||||||
|
data_version = data["__version"]
|
||||||
else:
|
else:
|
||||||
self._logger.info("Can't determine version of OctoPrint version cache was created for, not using it")
|
self._logger.info("Can't determine version of OctoPrint version cache was created for, not using it")
|
||||||
return
|
return
|
||||||
|
|
@ -118,24 +123,18 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
||||||
self._logger.exception("Error parsing in version cache data")
|
self._logger.exception("Error parsing in version cache data")
|
||||||
|
|
||||||
def _save_version_cache(self):
|
def _save_version_cache(self):
|
||||||
import tempfile
|
|
||||||
import yaml
|
import yaml
|
||||||
import shutil
|
from octoprint.util import atomic_write
|
||||||
|
from octoprint._version import get_versions
|
||||||
|
|
||||||
file_obj = tempfile.NamedTemporaryFile(delete=False)
|
octoprint_version = get_versions()["version"]
|
||||||
try:
|
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)
|
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._version_cache_dirty = False
|
||||||
self._logger.info("Saved version cache to disk")
|
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)))
|
|
||||||
|
|
||||||
#~~ SettingsPlugin API
|
#~~ SettingsPlugin API
|
||||||
|
|
||||||
|
|
@ -159,20 +158,62 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
||||||
data = dict(octoprint.plugin.SettingsPlugin.on_settings_load(self))
|
data = dict(octoprint.plugin.SettingsPlugin.on_settings_load(self))
|
||||||
if "checks" in data:
|
if "checks" in data:
|
||||||
del data["checks"]
|
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
|
return data
|
||||||
|
|
||||||
def on_settings_save(self, data):
|
def on_settings_save(self, data):
|
||||||
for key in self.get_settings_defaults():
|
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
|
continue
|
||||||
if key in data:
|
if key in data:
|
||||||
self._settings.set([key], data[key])
|
self._settings.set([key], data[key])
|
||||||
|
|
||||||
if "cache_ttl" in data:
|
if "cache_ttl" in data:
|
||||||
self._settings.set_int(["cache_ttl"], data["cache_ttl"])
|
self._settings.set_int(["cache_ttl"], data["cache_ttl"])
|
||||||
|
|
||||||
self._version_cache_ttl = self._settings.get_int(["cache_ttl"]) * 60
|
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):
|
def get_settings_version(self):
|
||||||
return 4
|
return 4
|
||||||
|
|
||||||
|
|
@ -380,7 +421,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
||||||
try:
|
try:
|
||||||
target_information, target_update_available, target_update_possible = self._get_current_version(target, populated_check, force=force)
|
target_information, target_update_available, target_update_possible = self._get_current_version(target, populated_check, force=force)
|
||||||
if target_information is None:
|
if target_information is None:
|
||||||
continue
|
target_information = dict()
|
||||||
except exceptions.UnknownCheckType:
|
except exceptions.UnknownCheckType:
|
||||||
self._logger.warn("Unknown update check type for %s" % target)
|
self._logger.warn("Unknown update check type for %s" % target)
|
||||||
continue
|
continue
|
||||||
|
|
@ -399,22 +440,29 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
||||||
updatePossible=target_update_possible,
|
updatePossible=target_update_possible,
|
||||||
information=target_information,
|
information=target_information,
|
||||||
displayName=populated_check["displayName"],
|
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:
|
if self._version_cache_dirty:
|
||||||
self._save_version_cache()
|
self._save_version_cache()
|
||||||
return information, update_available, update_possible
|
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):
|
def _get_current_version(self, target, check, force=False):
|
||||||
"""
|
"""
|
||||||
Determines the current version information for one target based on its check configuration.
|
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:
|
if target in self._version_cache and not force:
|
||||||
timestamp, information, update_available, update_possible = self._version_cache[target]
|
data = self._version_cache[target]
|
||||||
if timestamp + self._version_cache_ttl >= time.time() > timestamp:
|
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
|
# 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()
|
information = dict()
|
||||||
update_available = False
|
update_available = False
|
||||||
|
|
@ -437,7 +485,11 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
||||||
except:
|
except:
|
||||||
update_possible = False
|
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
|
self._version_cache_dirty = True
|
||||||
return information, update_available, update_possible
|
return information, update_available, update_possible
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,20 @@ $(function() {
|
||||||
self.workingOutput = undefined;
|
self.workingOutput = undefined;
|
||||||
self.loglines = ko.observableArray([]);
|
self.loglines = ko.observableArray([]);
|
||||||
|
|
||||||
|
self.octoprintUnconfigured = ko.observable();
|
||||||
|
self.octoprintUnreleased = ko.observable();
|
||||||
|
|
||||||
self.config_cacheTtl = ko.observable();
|
self.config_cacheTtl = ko.observable();
|
||||||
|
self.config_checkoutFolder = ko.observable();
|
||||||
|
self.config_checkType = ko.observable();
|
||||||
|
|
||||||
self.configurationDialog = $("#settings_plugin_softwareupdate_configurationdialog");
|
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(
|
self.versions = new ItemListHelper(
|
||||||
"plugin.softwareupdate.versions",
|
"plugin.softwareupdate.versions",
|
||||||
{
|
{
|
||||||
|
|
@ -82,15 +92,23 @@ $(function() {
|
||||||
var data = {
|
var data = {
|
||||||
plugins: {
|
plugins: {
|
||||||
softwareupdate: {
|
softwareupdate: {
|
||||||
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._copyConfig = function() {
|
||||||
self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl());
|
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) {
|
self.fromCheckResponse = function(data, ignoreSeen, showIfNothingNew) {
|
||||||
|
|
@ -109,6 +127,25 @@ $(function() {
|
||||||
});
|
});
|
||||||
self.versions.updateItems(versions);
|
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") {
|
if (data.status == "updateAvailable" || data.status == "updatePossible") {
|
||||||
var text = gettext("There are updates available for the following components:");
|
var text = gettext("There are updates available for the following components:");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,21 @@
|
||||||
|
<div class="alert" data-bind="visible: octoprintUnconfigured()">{% trans %}
|
||||||
|
Please configure the <strong>checkout folder</strong> of OctoPrint, otherwise
|
||||||
|
this plugin won't be able to update it. Click on the <i class="icon-wrench"></i> button
|
||||||
|
to do this. Also refer to the <a href="https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update" target="_blank">Documentation</a>.
|
||||||
|
{% endtrans %}</div>
|
||||||
|
<div class="alert" data-bind="visible: !octoprintUnconfigured() && octoprintUnreleased()">{% trans %}
|
||||||
|
<p>
|
||||||
|
<strong>You are running a non-release version of OctoPrint but are tracking OctoPrint
|
||||||
|
releases.</strong>
|
||||||
|
</p><p>
|
||||||
|
You probably want OctoPrint to track the matching development version instead.
|
||||||
|
If you have a local OctoPrint checkout folder switched to another branch,
|
||||||
|
<strong>simply switching over to "Commit" tracking</strong> will already
|
||||||
|
take care of that. Otherwise please take a look at the
|
||||||
|
<a href="https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update" target="_blank">Documentation</a>.
|
||||||
|
</p>
|
||||||
|
{% endtrans %}</div>
|
||||||
|
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<button class="btn btn-small" data-bind="click: function() { $root.showPluginSettings(); }" title="{{ _('Plugin Configuration') }}"><i class="icon-wrench"></i></button>
|
<button class="btn btn-small" data-bind="click: function() { $root.showPluginSettings(); }" title="{{ _('Plugin Configuration') }}"><i class="icon-wrench"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -11,7 +29,7 @@
|
||||||
<span data-bind="invisible: !updateAvailable"><i class="icon-bell" title="{{ _('Update available') }}"></i></span>
|
<span data-bind="invisible: !updateAvailable"><i class="icon-bell" title="{{ _('Update available') }}"></i></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="settings_plugin_softwareupdate_column_information">
|
<td class="settings_plugin_softwareupdate_column_information">
|
||||||
<strong data-bind="text: displayName"></strong>: <span data-bind="text: displayVersion"></span><br>
|
<strong data-bind="text: displayName"></strong>: <span data-bind="text: displayVersion"></span> <span data-bind="invisible: updatePossible"><i class="icon-exclamation-sign" title="{{ _('Update not possible, configuration ok?') }}"></i></span><br>
|
||||||
<small class="muted">
|
<small class="muted">
|
||||||
{{ _('Installed:') }} <span data-bind="text: information.local.name"></span><br>
|
{{ _('Installed:') }} <span data-bind="text: information.local.name"></span><br>
|
||||||
{{ _('Available:') }} <span data-bind="text: information.remote.name"></span>
|
{{ _('Available:') }} <span data-bind="text: information.remote.name"></span>
|
||||||
|
|
@ -44,7 +62,7 @@
|
||||||
<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><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">
|
<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" 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>
|
<button class="btn btn-block btn-danger" data-bind="visible: CONFIG_DEBUG, click: function() { $root.update(true); }">{{ _('Force update now (even if no new versions are available)') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -55,6 +73,18 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label">{{ _('OctoPrint checkout folder') }}</label>
|
||||||
|
<div class="controls">
|
||||||
|
<input type="text" class="input-block-level" data-bind="value: config_checkoutFolder">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label">{{ _('OctoPrint version tracking') }}</label>
|
||||||
|
<div class="controls">
|
||||||
|
<select data-bind="value: config_checkType, options: config_availableCheckTypes, optionsText: 'name', optionsValue: 'key'"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label class="control-label">{{ _('Version cache TTL') }}</label>
|
<label class="control-label">{{ _('Version cache TTL') }}</label>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,17 @@ def _get_caller(log_cb=None):
|
||||||
|
|
||||||
|
|
||||||
def can_perform_update(target, check):
|
def can_perform_update(target, check):
|
||||||
return "update_script" in check and ("checkout_folder" in check or "update_folder" in check)
|
import os
|
||||||
|
script_configured = bool("update_script" in check and check["update_script"])
|
||||||
|
|
||||||
|
folder = None
|
||||||
|
if "update_folder" in check:
|
||||||
|
folder = check["update_folder"]
|
||||||
|
elif "checkout_folder" in check:
|
||||||
|
folder = check["checkout_folder"]
|
||||||
|
folder_configured = bool(folder and os.path.isdir(folder))
|
||||||
|
|
||||||
|
return script_configured and folder_configured
|
||||||
|
|
||||||
|
|
||||||
def perform_update(target, check, target_version, log_cb=None):
|
def perform_update(target, check, target_version, log_cb=None):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue