diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py
index bc8903b7..2fdfeb89 100644
--- a/src/octoprint/filemanager/__init__.py
+++ b/src/octoprint/filemanager/__init__.py
@@ -136,8 +136,16 @@ class FileManager(object):
def initialize(self):
self.reload_plugins()
- for storage_type, storage_manager in self._storage_managers.items():
- self._determine_analysis_backlog(storage_type, storage_manager)
+
+ def worker():
+ self._logger.info("Adding backlog items from all storage types to analysis queue...".format(**locals()))
+ for storage_type, storage_manager in self._storage_managers.items():
+ self._determine_analysis_backlog(storage_type, storage_manager)
+
+ import threading
+ thread = threading.Thread(target=worker)
+ thread.daemon = True
+ thread.start()
def reload_plugins(self):
self._progress_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.ProgressPlugin)
@@ -150,13 +158,15 @@ class FileManager(object):
self._slicing_progress_callbacks.remove(callback)
def _determine_analysis_backlog(self, storage_type, storage_manager):
- self._logger.info("Adding backlog items from {storage_type} to analysis queue".format(**locals()))
+ counter = 0
for entry, path, printer_profile in storage_manager.analysis_backlog:
file_type = get_file_type(path)[-1]
# we'll use the default printer profile for the backlog since we don't know better
queue_entry = QueueEntry(entry, file_type, storage_type, path, self._printer_profile_manager.get_default())
self._analysis_queue.enqueue(queue_entry, high_priority=False)
+ counter += 1
+ self._logger.info("Added {counter} items from storage type \"{storage_type}\" to analysis queue".format(**locals()))
def add_storage(self, storage_type, storage_manager):
self._storage_managers[storage_type] = storage_manager
diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py
index bd81df0e..6e855cce 100644
--- a/src/octoprint/filemanager/storage.py
+++ b/src/octoprint/filemanager/storage.py
@@ -309,6 +309,41 @@ class LocalFileStorage(StorageInterface):
self._metadata_cache = pylru.lrucache(10)
+ self._old_metadata = None
+ self._initialize_metadata()
+
+ def _initialize_metadata(self):
+ self._logger.info("Initializing the file metadata for {}...".format(self.basefolder))
+
+ old_metadata_path = os.path.join(self.basefolder, "metadata.yaml")
+ backup_path = os.path.join(self.basefolder, "metadata.yaml.backup")
+
+ if os.path.exists(old_metadata_path):
+ # load the old metadata file
+ try:
+ with open(old_metadata_path) as f:
+ import yaml
+ self._old_metadata = yaml.safe_load(f)
+ except:
+ self._logger.exception("Error while loading old metadata file")
+
+ # make sure the metadata is initialized as far as possible
+ self._list_folder(self.basefolder)
+
+ # rename the old metadata file
+ self._old_metadata = None
+ try:
+ import shutil
+ shutil.move(old_metadata_path, backup_path)
+ except:
+ self._logger.exception("Could not rename old metadata.yaml file")
+
+ else:
+ # make sure the metadata is initialized as far as possible
+ self._list_folder(self.basefolder)
+
+ self._logger.info("... file metadata for {} initialized successfully.".format(self.basefolder))
+
@property
def analysis_backlog(self):
for entry in self._analysis_backlog_generator():
@@ -919,12 +954,7 @@ class LocalFileStorage(StorageInterface):
if entry in metadata and isinstance(metadata[entry], dict):
entry_data = metadata[entry]
else:
- entry_data = dict(
- hash=self._create_hash(entry_path),
- links=[],
- notes=[]
- )
- metadata[entry] = entry_data
+ entry_data = self._add_basic_metadata(path, entry, save=False, metadata=metadata)
metadata_dirty = True
# TODO extract model hash from source if possible to recreate link
@@ -959,6 +989,31 @@ class LocalFileStorage(StorageInterface):
return result
+ def _add_basic_metadata(self, path, entry, additional_metadata=None, save=True, metadata=None):
+ if additional_metadata is None:
+ additional_metadata = dict()
+
+ if metadata is None:
+ metadata = self._get_metadata(path)
+
+ entry_data = dict(
+ hash=self._create_hash(os.path.join(path, entry)),
+ links=[],
+ notes=[]
+ )
+
+ if path == self.basefolder and self._old_metadata is not None and entry in self._old_metadata and "gcodeAnalysis" in self._old_metadata[entry]:
+ # if there is still old metadata available and that contains an analysis for this file, use it!
+ entry_data["analysis"] = self._old_metadata[entry]["gcodeAnalysis"]
+
+ entry_data.update(additional_metadata)
+ metadata[entry] = entry_data
+
+ if save:
+ self._save_metadata(path, metadata)
+
+ return entry_data
+
def _create_hash(self, path):
import hashlib
@@ -993,16 +1048,22 @@ class LocalFileStorage(StorageInterface):
def _save_metadata(self, path, metadata):
metadata_path = os.path.join(path, ".metadata.yaml")
- fh, metadata_temporary_path = tempfile.mkstemp()
- os.close(fh)
-
with self._metadata_lock:
try:
- with open(metadata_temporary_path, "w") as f:
- import yaml
- yaml.safe_dump(metadata, stream=f, default_flow_style=False, indent=" ", allow_unicode=True)
+ import yaml
import shutil
- shutil.move(metadata_temporary_path, metadata_path)
+
+ file_obj = tempfile.NamedTemporaryFile(delete=False)
+ try:
+ yaml.safe_dump(metadata, stream=file_obj, default_flow_style=False, indent=" ", allow_unicode=True)
+ file_obj.close()
+ shutil.move(file_obj.name, metadata_path)
+ 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)))
except:
self._logger.exception("Error while writing .metadata.yaml to {path}".format(**locals()))
else:
diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py
index 6477da92..4dde566f 100644
--- a/src/octoprint/plugin/__init__.py
+++ b/src/octoprint/plugin/__init__.py
@@ -281,6 +281,7 @@ class PluginSettings(object):
defaults = dict()
self.defaults = dict(plugins=dict())
self.defaults["plugins"][plugin_key] = defaults
+ self.defaults["plugins"][plugin_key]["_config_version"] = None
if get_preprocessors is None:
get_preprocessors = dict()
@@ -309,11 +310,17 @@ class PluginSettings(object):
return result
def add_getter_kwargs(kwargs):
- kwargs.update(defaults=self.defaults, preprocessors=self.get_preprocessors)
+ if not "defaults" in kwargs:
+ kwargs.update(defaults=self.defaults)
+ if not "preprocessors" in kwargs:
+ kwargs.update(preprocessors=self.get_preprocessors)
return kwargs
def add_setter_kwargs(kwargs):
- kwargs.update(defaults=self.defaults, preprocessors=self.set_preprocessors)
+ if not "defaults" in kwargs:
+ kwargs.update(defaults=self.defaults)
+ if not "preprocessors" in kwargs:
+ kwargs.update(preprocessors=self.set_preprocessors)
return kwargs
self.access_methods = dict(
diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py
index d5c59bd9..8a505372 100644
--- a/src/octoprint/plugin/core.py
+++ b/src/octoprint/plugin/core.py
@@ -428,7 +428,9 @@ class PluginManager(object):
It is able to discover plugins both through possible file system locations as well as customizable entry points.
"""
- def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None, plugin_disabled_list=None, plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, plugin_validators=None):
+ def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None,
+ plugin_disabled_list=None, plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None,
+ plugin_validators=None):
self.logger = logging.getLogger(__name__)
if logging_prefix is None:
@@ -453,6 +455,8 @@ class PluginManager(object):
self.implementation_injects = dict()
self.implementation_inject_factories = []
+ self.implementation_pre_inits = []
+ self.implementation_post_inits = []
self.on_plugin_loaded = lambda *args, **kwargs: None
self.on_plugin_unloaded = lambda *args, **kwargs: None
@@ -849,27 +853,35 @@ class PluginManager(object):
return False
return hook in self.plugin_obsolete_hooks
- def initialize_implementations(self, additional_injects=None, additional_inject_factories=None):
+ def initialize_implementations(self, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None):
for name, plugin in self.enabled_plugins.items():
self.initialize_implementation_of_plugin(name, plugin,
additional_injects=additional_injects,
- additional_inject_factories=additional_inject_factories)
+ additional_inject_factories=additional_inject_factories,
+ additional_pre_inits=additional_pre_inits,
+ additional_post_inits=additional_post_inits)
self.logger.info("Initialized {count} plugin(s)".format(count=len(self.plugin_implementations)))
- def initialize_implementation_of_plugin(self, name, plugin, additional_injects=None, additional_inject_factories=None):
+ def initialize_implementation_of_plugin(self, name, plugin, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None):
if plugin.implementation is None:
return
return self.initialize_implementation(name, plugin, plugin.implementation,
additional_injects=additional_injects,
- additional_inject_factories=additional_inject_factories)
+ additional_inject_factories=additional_inject_factories,
+ additional_pre_inits=additional_pre_inits,
+ additional_post_inits=additional_post_inits)
- def initialize_implementation(self, name, plugin, implementation, additional_injects=None, additional_inject_factories=None):
+ def initialize_implementation(self, name, plugin, implementation, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None):
if additional_injects is None:
additional_injects = dict()
if additional_inject_factories is None:
additional_inject_factories = []
+ if additional_pre_inits is None:
+ additional_pre_inits = []
+ if additional_post_inits is None:
+ additional_post_inits = []
injects = self.implementation_injects
injects.update(additional_injects)
@@ -877,6 +889,12 @@ class PluginManager(object):
inject_factories = self.implementation_inject_factories
inject_factories += additional_inject_factories
+ pre_inits = self.implementation_pre_inits
+ pre_inits += additional_pre_inits
+
+ post_inits = self.implementation_post_inits
+ post_inits += additional_post_inits
+
try:
kwargs = dict(injects)
@@ -904,8 +922,16 @@ class PluginManager(object):
for arg, value in return_value.items():
setattr(implementation, "_" + arg, value)
+ # execute any additional pre init methods
+ for pre_init in pre_inits:
+ pre_init(name, implementation)
+
implementation.initialize()
+ # execute any additional post init methods
+ for post_init in post_inits:
+ post_init(name, implementation)
+
except Exception as e:
self._deactivate_plugin(name, plugin)
plugin.enabled = False
@@ -1189,13 +1215,13 @@ class RestartNeedingPlugin(Plugin):
class PluginNeedsRestart(BaseException):
def __init__(self, name):
- super(BaseException, self).__init__()
+ BaseException.__init__(self)
self.name = name
self.message = "Plugin {name} cannot be enabled or disabled after system startup".format(**locals())
class PluginLifecycleException(BaseException):
def __init__(self, name, reason, message):
- super(BaseException, self).__init__()
+ BaseException.__init__(self)
self.name = name
self.reason = reason
@@ -1206,7 +1232,7 @@ class PluginLifecycleException(BaseException):
class PluginCantInitialize(PluginLifecycleException):
def __init__(self, name, reason):
- super(PluginLifecycleException, self).__init__(name, reason, "Plugin {name} cannot be initialized: {reason}")
+ PluginLifecycleException.__init__(self, name, reason, "Plugin {name} cannot be initialized: {reason}")
class PluginCantEnable(PluginLifecycleException):
def __init__(self, name, reason):
@@ -1214,4 +1240,4 @@ class PluginCantEnable(PluginLifecycleException):
class PluginCantDisable(PluginLifecycleException):
def __init__(self, name, reason):
- super(PluginLifecycleException, self).__init__(name, reason, "Plugin {name} cannot be disabled: {reason}")
+ PluginLifecycleException.__init__(self, name, reason, "Plugin {name} cannot be disabled: {reason}")
diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py
index 842e68f9..d285173c 100644
--- a/src/octoprint/plugin/types.py
+++ b/src/octoprint/plugin/types.py
@@ -718,7 +718,7 @@ class SettingsPlugin(OctoPrintPlugin):
def on_settings_save(self, data):
old_flag = self._settings.get_boolean(["sub", "some_flag"])
- super(MySettingsPlugin, self).on_settings_save(data)
+ octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
new_flag = self._settings.get_boolean(["sub", "some_flag"])
if old_flag != new_flag:
@@ -830,6 +830,41 @@ class SettingsPlugin(OctoPrintPlugin):
"""
return dict(), dict()
+ def get_settings_version(self):
+ """
+ Retrieves the settings format version of the plugin.
+
+ Use this to have OctoPrint trigger your migration function if it detects an outdated settings version in
+ config.yaml.
+
+ Returns:
+ int or None: an int signifying the current settings format, should be incremented by plugins whenever there
+ are backwards incompatible changes. Returning None here disables the version tracking for the
+ plugin's configuration.
+ """
+ return None
+
+ def on_settings_migrate(self, target, current):
+ """
+ Called by OctoPrint if it detects that the installed version of the plugin necessitates a higher settings version
+ than the one currently stored in _config.yaml. Will also be called if the settings data stored in config.yaml
+ doesn't have version information, in which case the ``current`` parameter will be None.
+
+ Your plugin's implementation should take care of migrating any data by utilizing self._settings. OctoPrint
+ will take care of saving any changes to disk by calling `self._settings.save()` after returning from this method.
+
+ This method will be called before your plugin's :func:`initialize` method, but with all injections already
+ having taken place. You can therefore depend on the configuration having been migrated by the time :func:`initialize`
+ is called.
+
+ Arguments:
+ target (int): The settings format version the plugin requires, this should always be the same value as
+ returned by :func:`get_settings_version`.
+ current (int or None): The settings format version as currently stored in config.yaml. May be None if
+ no version information can be found.
+ """
+ pass
+
class EventHandlerPlugin(OctoPrintPlugin):
"""
diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py
index b1d0087d..e1ce090c 100644
--- a/src/octoprint/plugins/cura/__init__.py
+++ b/src/octoprint/plugins/cura/__init__.py
@@ -129,7 +129,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
def on_settings_save(self, data):
old_debug_logging = self._settings.get_boolean(["debug_logging"])
- super(CuraPlugin, self).on_settings_save(data)
+ octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
new_debug_logging = self._settings.get_boolean(["debug_logging"])
if old_debug_logging != new_debug_logging:
@@ -410,4 +410,4 @@ __plugin_author__ = "Gina Häußge"
__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura"
__plugin_description__ = "Adds support for slicing via CuraEngine from within OctoPrint"
__plugin_license__ = "AGPLv3"
-__plugin_implementation__ = CuraPlugin()
\ No newline at end of file
+__plugin_implementation__ = CuraPlugin()
diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py
index 495f0b83..e7bde03c 100644
--- a/src/octoprint/plugins/pluginmanager/__init__.py
+++ b/src/octoprint/plugins/pluginmanager/__init__.py
@@ -133,7 +133,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
result.append(self._to_external_representation(plugin))
if "refresh_repository" in request.values and request.values["refresh_repository"] in valid_boolean_trues:
- self._refresh_repository()
+ self._repository_available = self._refresh_repository()
return jsonify(plugins=result, repository=dict(available=self._repository_available, plugins=self._repository_plugins), os=self._get_os(), octoprint=self._get_octoprint_version())
@@ -141,6 +141,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
if not admin_permission.can():
return make_response("Insufficient rights", 403)
+ if self._printer.is_printing() or self._printer.is_paused():
+ # do not update while a print job is running
+ return make_response("Printer is currently printing or paused", 409)
+
if command == "install":
url = data["url"]
plugin_name = data["plugin"] if "plugin" in data else None
@@ -165,10 +169,6 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
plugin = self._plugin_manager.plugins[plugin_name]
return self.command_toggle(plugin, command)
- elif command == "refresh_repository":
- self._repository_available = self._refresh_repository()
- return jsonify(repository=dict(available=self._repository_available, plugins=self._repository_plugins))
-
def command_install(self, url=None, path=None, force=False, reinstall=None, dependency_links=False):
if url is not None:
pip_args = ["install", sarge.shell_quote(url)]
diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js
index ca7eaae9..c8178d20 100644
--- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js
+++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js
@@ -4,6 +4,7 @@ $(function() {
self.loginState = parameters[0];
self.settingsViewModel = parameters[1];
+ self.printerState = parameters[2];
self.plugins = new ItemListHelper(
"plugin.pluginmanager.installedplugins",
@@ -76,6 +77,22 @@ $(function() {
self.workingDialog = undefined;
self.workingOutput = undefined;
+ self.enableManagement = ko.computed(function() {
+ return !self.printerState.isPrinting();
+ });
+
+ self.enableToggle = function(data) {
+ return self.enableManagement() && data.key != 'pluginmanager';
+ };
+
+ self.enableUninstall = function(data) {
+ return self.enableManagement() && !data.bundled && data.key != 'pluginmanager' && !data.pending_uninstall;
+ };
+
+ self.enableRepoInstall = function(data) {
+ return self.enableManagement() && self.isCompatible(data);
+ };
+
self.invalidUrl = ko.computed(function() {
var url = self.installUrl();
return url !== undefined && url.trim() != "" && !(_.startsWith(url.toLocaleLowerCase(), "http://") || _.startsWith(url.toLocaleLowerCase(), "https://"));
@@ -83,7 +100,7 @@ $(function() {
self.enableUrlInstall = ko.computed(function() {
var url = self.installUrl();
- return url !== undefined && url.trim() != "" && !self.invalidUrl();
+ return self.enableManagement() && url !== undefined && url.trim() != "" && !self.invalidUrl();
});
self.invalidArchive = ko.computed(function() {
@@ -93,7 +110,7 @@ $(function() {
self.enableArchiveInstall = ko.computed(function() {
var name = self.uploadFilename();
- return name !== undefined && name.trim() != "" && !self.invalidArchive();
+ return self.enableManagement() && name !== undefined && name.trim() != "" && !self.invalidArchive();
});
self.uploadElement.fileupload({
@@ -187,6 +204,10 @@ $(function() {
return;
}
+ if (!self.enableManagement()) {
+ return;
+ }
+
if (data.key == "pluginmanager") return;
var command = self._getToggleCommand(data);
@@ -217,6 +238,10 @@ $(function() {
return;
}
+ if (!self.enableManagement()) {
+ return;
+ }
+
if (self.installed(data)) {
self.installPlugin(data.archive, data.title, data.id, data.follow_dependency_links || self.followDependencyLinks());
} else {
@@ -229,6 +254,10 @@ $(function() {
return;
}
+ if (!self.enableManagement()) {
+ return;
+ }
+
if (url === undefined) {
url = self.installUrl();
}
@@ -279,6 +308,10 @@ $(function() {
return;
}
+ if (!self.enableManagement()) {
+ return;
+ }
+
if (data.bundled) return;
if (data.key == "pluginmanager") return;
@@ -305,9 +338,7 @@ $(function() {
return;
}
- self._postCommand("refresh_repository", {}, function(data) {
- self._fromRepositoryResponse(data.repository);
- })
+ self.requestData(true);
};
self.installed = function(data) {
@@ -420,7 +451,7 @@ $(function() {
self.toggleButtonCss = function(data) {
var icon = self._getToggleCommand(data) == "enable" ? "icon-circle-blank" : "icon-circle";
- var disabled = (data.key == "pluginmanager") ? " disabled" : "";
+ var disabled = (self.enableToggle(data)) ? "" : " disabled";
return icon + disabled;
};
@@ -578,5 +609,5 @@ $(function() {
}
// view model class, parameters for constructor, container to bind to
- ADDITIONAL_VIEWMODELS.push([PluginManagerViewModel, ["loginStateViewModel", "settingsViewModel"], "#settings_plugin_pluginmanager"]);
+ ADDITIONAL_VIEWMODELS.push([PluginManagerViewModel, ["loginStateViewModel", "settingsViewModel", "printerStateViewModel"], "#settings_plugin_pluginmanager"]);
});
diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2
index f9710de0..8cffb17f 100644
--- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2
+++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2
@@ -1,3 +1,11 @@
+{% macro pluginmanager_printing() %}
+
+ {{ _('Take note that all plugin management functionality is disabled while your printer is printing.') }}
+
+{% endmacro %}
+
+{{ pluginmanager_printing() }}
+
{{ _('Installed Plugins') }}
@@ -20,7 +28,7 @@
- |
+ |
@@ -58,6 +66,7 @@
{{ _('Install new Plugins...') }}
+ {{ pluginmanager_printing() }}
-
+
diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py
index 7d74ff2e..0d2c96af 100644
--- a/src/octoprint/plugins/softwareupdate/__init__.py
+++ b/src/octoprint/plugins/softwareupdate/__init__.py
@@ -43,6 +43,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
def refresh_checks(name, plugin):
self._refresh_configured_checks = True
+ self._send_client_message("update_versions")
+
self._plugin_lifecycle_manager.add_callback("enabled", refresh_checks)
self._plugin_lifecycle_manager.add_callback("disabled", refresh_checks)
@@ -86,12 +88,76 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
}
def on_settings_save(self, data):
- super(SoftwareUpdatePlugin, self).on_settings_save(data)
+ octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
self._version_cache_ttl = self._settings.get_int(["cache_ttl"]) * 60
+ def get_settings_version(self):
+ return 1
+
+ def on_settings_migrate(self, target, current=None):
+ if current is None:
+ # there might be some left over data from the time we still persisted everything to settings,
+ # even the stuff that shouldn't be persisted but always provided by the hook - let's
+ # clean up
+
+ # take care of the octoprint entry
+ configured_checks = self._settings.get(["checks"], merged=True)
+ octoprint_check = dict(configured_checks["octoprint"])
+ if "type" in octoprint_check and not octoprint_check["type"] == "github_commit":
+ deletables=["current"]
+ else:
+ deletables=[]
+ octoprint_check = self._clean_settings_check("octoprint", octoprint_check, self.get_settings_defaults()["checks"]["octoprint"], delete=deletables, save=False)
+ configured_checks["octoprint"] = octoprint_check
+
+ # and the hooks
+ 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 configured_checks:
+ settings_check = dict(configured_checks[key])
+ merged = dict_merge(data, settings_check)
+ if "type" in merged and not merged["type"] == "github_commit":
+ deletables = ["current", "displayVersion"]
+ else:
+ deletables = []
+
+ self._clean_settings_check(key, settings_check, data, delete=deletables, save=False)
+
+ def _clean_settings_check(self, key, data, defaults, delete=None, save=True):
+ if delete is None:
+ delete = []
+
+ for k, v in data.items():
+ if k in defaults and defaults[k] == data[k]:
+ del data[k]
+
+ for k in delete:
+ if k in data:
+ del data[k]
+
+ dummy_defaults = dict(plugins=dict())
+ dummy_defaults["plugins"][self._identifier] = dict(checks=dict())
+ dummy_defaults["plugins"][self._identifier]["checks"][key] = defaults
+ if len(data):
+ self._settings.set(["checks", key], data, defaults=dummy_defaults)
+ else:
+ self._settings.set(["checks", key], None, defaults=dummy_defaults)
+
+ if save:
+ self._settings.save()
+
+ return data
+
#~~ BluePrint API
@octoprint.plugin.BlueprintPlugin.route("/check", methods=["GET"])
+ @restricted_access
def check_for_update(self):
if "check" in flask.request.values:
check_targets = map(str.strip, flask.request.values["check"].split(","))
@@ -101,7 +167,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
if "force" in flask.request.values and flask.request.values["force"] in octoprint.settings.valid_boolean_trues:
force = True
else:
- force=False
+ force = False
try:
information, update_available, update_possible = self.get_current_versions(check_targets=check_targets, force=force)
@@ -116,10 +182,10 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
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)
+ return 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)
+ return flask.make_response("Expected content-type JSON", 400)
json_data = flask.request.json
@@ -128,9 +194,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
else:
check_targets = None
- if "force" in json_data:
- from octoprint.settings import valid_boolean_trues
- force = (json_data["force"] in valid_boolean_trues)
+ if "force" in json_data and json_data["force"] in octoprint.settings.valid_boolean_trues:
+ force = True
else:
force = False
@@ -378,15 +443,14 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
# 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)
+ dummy_default = dict(plugins=dict())
+ dummy_default["plugins"][self._identifier] = dict(checks=dict())
+ dummy_default["plugins"][self._identifier]["checks"][target] = dict(current=None)
+ self._settings.set(["checks", target, "current"], target_version, defaults=dummy_default)
- # 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()
+ # 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
diff --git a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js
index b8f0c3c6..19c88137 100644
--- a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js
+++ b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js
@@ -44,9 +44,7 @@ $(function() {
};
self._showPopup = function(options, eventListeners) {
- if (self.popup !== undefined) {
- self.popup.remove();
- }
+ self._closePopup();
self.popup = new PNotify(options);
if (eventListeners) {
@@ -65,6 +63,12 @@ $(function() {
}
};
+ self._closePopup = function() {
+ if (self.popup !== undefined) {
+ self.popup.remove();
+ }
+ };
+
self.showPluginSettings = function() {
self._copyConfig();
self.configurationDialog.modal();
@@ -89,6 +93,86 @@ $(function() {
self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl());
};
+ self.fromCheckResponse = function(data, ignoreSeen, showIfNothingNew) {
+ 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 += "";
+ _.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 += "" + displayName + (update_info.updatePossible ? " " : "") + " ";
+ }
+ });
+ text += " ";
+
+ text += "" + gettext("Those components marked with can be updated directly.") + " ";
+
+ 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") {
+ if (showIfNothingNew) {
+ self._showPopup({
+ title: gettext("Everything is up-to-date"),
+ hide: false,
+ type: "success"
+ });
+ } else {
+ self._closePopup();
+ }
+ }
+ };
+
self.performCheck = function(showIfNothingNew, force, ignoreSeen) {
if (!self.loginState.isUser()) return;
@@ -102,79 +186,7 @@ $(function() {
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 += "";
- _.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 += "" + displayName + (update_info.updatePossible ? " " : "") + " ";
- }
- });
- text += " ";
-
- text += "" + gettext("Those components marked with can be updated directly.") + " ";
-
- 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.fromCheckResponse(data, ignoreSeen, showIfNothingNew);
}
});
};
@@ -421,6 +433,10 @@ $(function() {
self.updateInProgress = false;
break;
}
+ case "update_versions": {
+ self.performCheck();
+ break;
+ }
}
if (options != undefined) {
@@ -432,4 +448,4 @@ $(function() {
// view model class, parameters for constructor, container to bind to
ADDITIONAL_VIEWMODELS.push([SoftwareUpdateViewModel, ["loginStateViewModel", "printerStateViewModel", "settingsViewModel"], document.getElementById("settings_plugin_softwareupdate")]);
-});
\ No newline at end of file
+});
diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py
index 437333ed..d94ba4a4 100644
--- a/src/octoprint/server/__init__.py
+++ b/src/octoprint/server/__init__.py
@@ -210,7 +210,22 @@ class Server():
set_preprocessors=set_preprocessors)
return dict(settings=plugin_settings)
+ def settings_plugin_pre_init(name, implementation):
+ if not isinstance(implementation, octoprint.plugin.SettingsPlugin):
+ return
+
+ settings_version = implementation.get_settings_version()
+ settings_migrator = implementation.on_settings_migrate
+
+ if settings_version is not None and settings_migrator is not None:
+ stored_version = implementation._settings.get_int(["_config_version"])
+ if stored_version is None or stored_version < settings_version:
+ settings_migrator(settings_version, stored_version)
+ implementation._settings.set_int(["_config_version"], settings_version)
+ implementation._settings.save()
+
pluginManager.implementation_inject_factories=[octoprint_plugin_inject_factory, settings_plugin_inject_factory]
+ pluginManager.implementation_pre_inits=[settings_plugin_pre_init]
pluginManager.initialize_implementations()
pluginManager.log_all_plugins()
@@ -674,6 +689,20 @@ class Server():
base_folder = settings().getBaseFolder("generated")
+ # clean the folder
+ if settings().getBoolean(["devel", "webassets", "clean_on_startup"]):
+ import shutil
+ for entry in ("webassets", ".webassets-cache"):
+ path = os.path.join(base_folder, entry)
+ self._logger.debug("Deleting {path}...".format(**locals()))
+ if os.path.isdir(path):
+ shutil.rmtree(path, ignore_errors=True)
+ elif os.path.isfile(path):
+ try:
+ os.remove(path)
+ except:
+ self._logger.exception("Exception while trying to delete {entry} from {base_folder}".format(**locals()))
+
AdjustedEnvironment = type(Environment)(Environment.__name__, (Environment,), dict(
resolver_class=util.flask.PluginAssetResolver
))
@@ -754,16 +783,7 @@ class Server():
if len(less_app) == 0:
less_app = ["empty"]
- js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js")
- if settings().getBoolean(["devel", "webassets", "minify"]):
- js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="rjsmin")
- else:
- js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js")
-
- css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css")
- css_app_bundle = Bundle(*css_app, output="webassets/packed_app.css")
-
- from webassets.filter import register_filter
+ from webassets.filter import register_filter, Filter
from webassets.filter.cssrewrite.base import PatternRewriter
import re
class LessImportRewrite(PatternRewriter):
@@ -782,7 +802,24 @@ class Server():
return "{import_with_options}\"{import_url}\";".format(**locals())
+ class JsDelimiterBundle(Filter):
+ name = "js_delimiter_bundler"
+ options = {}
+ def input(self, _in, out, **kwargs):
+ out.write(_in.read())
+ out.write("\n;\n")
+
register_filter(LessImportRewrite)
+ register_filter(JsDelimiterBundle)
+
+ js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js", filters="js_delimiter_bundler")
+ if settings().getBoolean(["devel", "webassets", "minify"]):
+ js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="rjsmin, js_delimiter_bundler")
+ else:
+ js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="js_delimiter_bundler")
+
+ css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css")
+ css_app_bundle = Bundle(*css_app, output="webassets/packed_app.css")
all_less_bundle = Bundle(*less_app, output="webassets/packed_app.less", filters="less_importrewrite")
diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py
index 7bba8a02..187ff460 100644
--- a/src/octoprint/server/api/settings.py
+++ b/src/octoprint/server/api/settings.py
@@ -26,6 +26,8 @@ import octoprint.util
@api.route("/settings", methods=["GET"])
def getSettings():
+ logger = logging.getLogger(__name__)
+
s = settings()
connectionOptions = get_connection_options()
@@ -118,7 +120,7 @@ def getSettings():
for name in gcode_scripts:
data["scripts"]["gcode"][name] = s.loadScript("gcode", name, source=True)
- def process_plugin_result(name, plugin, result):
+ def process_plugin_result(name, result):
if result:
if not "plugins" in data:
data["plugins"] = dict()
@@ -126,9 +128,17 @@ def getSettings():
del result["__enabled"]
data["plugins"][name] = result
- octoprint.plugin.call_plugin(octoprint.plugin.SettingsPlugin,
- "on_settings_load",
- callback=process_plugin_result)
+ for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin):
+ try:
+ result = plugin.on_settings_load()
+ process_plugin_result(plugin._identifier, result)
+ except TypeError:
+ logger.warn("Could not load settings for plugin {name} ({version}) since it called super(...)".format(name=plugin._plugin_name, version=plugin._plugin_version))
+ logger.warn("in a way which has issues due to OctoPrint's dynamic reloading after plugin operations.")
+ logger.warn("Please contact the plugin's author and ask to update the plugin to use a direct call like")
+ logger.warn("octoprint.plugin.SettingsPlugin.on_settings_load(self) instead.")
+ except:
+ logger.exception("Could not load settings for plugin {name} ({version})".format(version=plugin._plugin_version, name=plugin._plugin_name))
return jsonify(data)
@@ -137,6 +147,8 @@ def getSettings():
@restricted_access
@admin_permission.require(403)
def setSettings():
+ logger = logging.getLogger(__name__)
+
if not "application/json" in request.headers["Content-Type"]:
return make_response("Expected content-type JSON", 400)
@@ -235,8 +247,15 @@ def setSettings():
for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin):
plugin_id = plugin._identifier
if plugin_id in data["plugins"]:
- plugin.on_settings_save(data["plugins"][plugin_id])
-
+ try:
+ plugin.on_settings_save(data["plugins"][plugin_id])
+ except TypeError:
+ logger.warn("Could not save settings for plugin {name} ({version}) since it called super(...)".format(name=plugin._plugin_name, version=plugin._plugin_version))
+ logger.warn("in a way which has issues due to OctoPrint's dynamic reloading after plugin operations.")
+ logger.warn("Please contact the plugin's author and ask to update the plugin to use a direct call like")
+ logger.warn("octoprint.plugin.SettingsPlugin.on_settings_save(self, data) instead.")
+ except:
+ logger.exception("Could not save settings for plugin {name} ({version})".format(version=plugin._plugin_version, name=plugin._plugin_name))
if s.save():
eventManager().fire(Events.SETTINGS_UPDATED)
diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py
index ab4e24f3..a024338b 100644
--- a/src/octoprint/settings.py
+++ b/src/octoprint/settings.py
@@ -251,7 +251,8 @@ default_settings = {
},
"webassets": {
"minify": False,
- "bundle": True
+ "bundle": True,
+ "clean_on_startup": True
},
"virtualPrinter": {
"enabled": False,
@@ -853,7 +854,7 @@ class Settings(object):
return results
def getInt(self, path, config=None, defaults=None, preprocessors=None):
- value = self.get(path, defaults=defaults, preprocessors=preprocessors)
+ value = self.get(path, config=config, defaults=defaults, preprocessors=preprocessors)
if value is None:
return None
@@ -930,14 +931,15 @@ class Settings(object):
#~~ setter
- def set(self, path, value, force=False, defaults=None, preprocessors=None):
+ def set(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
if len(path) == 0:
return
if self._mtime is not None and self.last_modified != self._mtime:
self.load()
- config = self._config
+ if config is None:
+ config = self._config
if defaults is None:
defaults = default_settings
if preprocessors is None:
@@ -973,9 +975,9 @@ class Settings(object):
config[key] = value
self._dirty = True
- def setInt(self, path, value, force=False, defaults=None, preprocessors=None):
+ def setInt(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
if value is None:
- self.set(path, None, force=force, defaults=defaults, preprocessors=preprocessors)
+ self.set(path, None, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
return
try:
@@ -984,11 +986,11 @@ class Settings(object):
self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path))
return
- self.set(path, intValue, force)
+ self.set(path, intValue, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
- def setFloat(self, path, value, force=False, defaults=None, preprocessors=None):
+ def setFloat(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
if value is None:
- self.set(path, None, force=force, defaults=defaults, preprocessors=preprocessors)
+ self.set(path, None, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
return
try:
@@ -997,15 +999,15 @@ class Settings(object):
self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path))
return
- self.set(path, floatValue, force)
+ self.set(path, floatValue, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
- def setBoolean(self, path, value, force=False, defaults=None, preprocessors=None):
+ def setBoolean(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
if value is None or isinstance(value, bool):
- self.set(path, value, force=force, defaults=defaults, preprocessors=preprocessors)
+ self.set(path, value, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
elif value.lower() in valid_boolean_trues:
- self.set(path, True, force=force, defaults=defaults, preprocessors=preprocessors)
+ self.set(path, True, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
else:
- self.set(path, False, force=force, defaults=defaults, preprocessors=preprocessors)
+ self.set(path, False, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
def setBaseFolder(self, type, path, force=False):
if type not in default_settings["folder"].keys():
diff --git a/src/octoprint/slicing/exceptions.py b/src/octoprint/slicing/exceptions.py
index c726f508..4d789107 100644
--- a/src/octoprint/slicing/exceptions.py
+++ b/src/octoprint/slicing/exceptions.py
@@ -54,7 +54,7 @@ class SlicerException(SlicingException):
Identifier of the slicer for which the exception was raised.
"""
def __init__(self, slicer, *args, **kwargs):
- super(SlicingException, self).__init__(*args, **kwargs)
+ SlicingException.__init__(self, *args, **kwargs)
self.slicer = slicer
class SlicerNotConfigured(SlicerException):
@@ -62,7 +62,7 @@ class SlicerNotConfigured(SlicerException):
Raised if a slicer is not yet configured but must be configured to proceed.
"""
def __init__(self, slicer, *args, **kwargs):
- super(SlicerException, self).__init__(slicer, *args, **kwargs)
+ SlicerException.__init__(self, slicer, *args, **kwargs)
self.message = "Slicer not configured: {slicer}".format(slicer=slicer)
class UnknownSlicer(SlicerException):
@@ -70,7 +70,7 @@ class UnknownSlicer(SlicerException):
Raised if a slicer is unknown.
"""
def __init__(self, slicer, *args, **kwargs):
- super(SlicerException, self).__init__(slicer, *args, **kwargs)
+ SlicerException.__init__(self, slicer, *args, **kwargs)
self.message = "No such slicer: {slicer}".format(slicer=slicer)
class ProfileException(BaseException):
@@ -86,7 +86,7 @@ class ProfileException(BaseException):
Identifier of the profile for which the exception was raised.
"""
def __init__(self, slicer, profile, *args, **kwargs):
- super(BaseException, self).__init__(*args, **kwargs)
+ BaseException.__init__(self, *args, **kwargs)
self.slicer = slicer
self.profile = profile
@@ -95,7 +95,7 @@ class UnknownProfile(ProfileException):
Raised if a slicing profile does not exist but must exist to proceed.
"""
def __init__(self, slicer, profile, *args, **kwargs):
- super(ProfileException, self).__init__(slicer, profile, *args, **kwargs)
+ ProfileException.__init__(self, slicer, profile, *args, **kwargs)
self.message = "Profile {profile} for slicer {slicer} does not exist".format(profile=profile, slicer=slicer)
class ProfileAlreadyExists(ProfileException):
@@ -103,5 +103,5 @@ class ProfileAlreadyExists(ProfileException):
Raised if a slicing profile already exists and must not be overwritten.
"""
def __init__(self, slicer, profile, *args, **kwargs):
- super(ProfileException, self).__init__(slicer, profile, *args, **kwargs)
- self.message = "Profile {profile} for slicer {slicer} already exists".format(profile=profile, slicer=slicer)
\ No newline at end of file
+ ProfileException.__init__(self, slicer, profile, *args, **kwargs)
+ self.message = "Profile {profile} for slicer {slicer} already exists".format(profile=profile, slicer=slicer)