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...') }}

- +
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 += ""; + + 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 += ""; - - 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)
-  |  +  |