diff --git a/.versioneer-lookup b/.versioneer-lookup index 7c9750a02..db6d9a7b7 100644 --- a/.versioneer-lookup +++ b/.versioneer-lookup @@ -10,10 +10,10 @@ # master shall not use the lookup table, only tags master -# maintenance is currently the branch for preparation of maintenance release 1.2.4 +# maintenance is currently the branch for preparation of maintenance release 1.2.5 # so are any fix/... branches -maintenance 1.2.4-dev 3761995aff94acf32495556730f133a1626245b3 -fix/.* 1.2.4-dev 3761995aff94acf32495556730f133a1626245b3 +maintenance 1.2.5-dev 9a6099ffc2982455d631c9d68a3273d9eb55885c +fix/.* 1.2.5-dev 9a6099ffc2982455d631c9d68a3273d9eb55885c # every other branch is a development branch and thus gets resolved to 1.3.0-dev for now .* 1.3.0-dev 198d3450d94be1a2 diff --git a/AUTHORS.md b/AUTHORS.md index 07f0fc9ab..0309fdf53 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -52,6 +52,7 @@ date of first contribution): * [Lucas Clemente](https://github.com/lucas-clemente) * [Andrew Erickson](https://github.com/aerickson) * [Nicanor Romero Venier](https://github.com/nicanor-romero) + * [Thomas Hou](https://github.com/masterhou) OctoPrint started off as a fork of [Cura](https://github.com/daid/Cura) by [Daid Braam](https://github.com/daid). Parts of its communication layer and diff --git a/CHANGELOG.md b/CHANGELOG.md index 11546dbf1..69ef9b242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,77 @@ # OctoPrint Changelog +## 1.2.5 (2015-08-31) + +### Improvements + + * [#986](https://github.com/foosel/OctoPrint/issues/986) - Added tooltip for + "additional data" button in file list. + * [#1028](https://github.com/foosel/OctoPrint/issues/1028) - Hint about why + timelapse configuration is disabled. + * New central configuration option for commands to restart OctoPrint and to + restart and shut down the system OctoPrint is running on. This allows plugins + (like the Software Update Plugin or the Plugin Manager) and core functionality + to perform these common administrative tasks without the user needing to define + everything redundantly. + * Settings dialog now visualizes when settings are saving and when they being + retrieved. Also the Send/Cancel buttons are disabled while settings are saving + to prevent duplicated requests and concurrent retrieval of the settings by + multiple viewmodels is disabled as well. + * Better protection against rendering errors from templates provided by third + party plugins. + * Better protection against corrupting the configuration by using a temporary + file as intermediate buffer. + * Added warning to UI regarding Z timelapses and spiralized objects. + * Better compatibility with Repetier firmware: + * Added "Format Error" to whitelisted recoverable communication errors + (see also [#1032](https://github.com/foosel/OctoPrint/pull/1032)). + * Added option to ignore repeated resend requests for the same line (see + also discussion in [#1015](https://github.com/foosel/OctoPrint/pull/1015)). + * Software Update Plugin: + * Adjusted to utilize new centralized restart commands (see above). + * Allow configuration of checkout folder and version tracking type via + Plugin Configuration. + * Display message to user if OctoPrint's 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. + * Made disk space running low a bit more obvious through visual warning on + configurable thresholds. + +### Bug Fixes + + * [#985](https://github.com/foosel/OctoPrint/issues/985) - Do not hiccup on + unset `Content-Type` part headers for multipart file uploads. + * [#1001](https://github.com/foosel/OctoPrint/issues/1001) - Fixed connection + tab not unfolding properly (see also [#1002](https://github.com/foosel/OctoPrint/pull/1002)). + * [#1012](https://github.com/foosel/OctoPrint/issues/1012) - All API + responses now set no-cache headers, making the Edge browser behave a bit better + * [#1019](https://github.com/foosel/OctoPrint/issues/1019) - Better error + handling of problems when trying to write the webassets cache. + * [#1021](https://github.com/foosel/OctoPrint/issues/1021) - Properly handle + serial close on Macs. + * [#1031](https://github.com/foosel/OctoPrint/issues/1031) - Special + handling of `M112` (emergency stop) command: + * Jump send queue + * In case the printer's firmware doesn't understand it yet, at least + shutdown all of the heaters + * Disconnect + * Properly reset job progress to 0% when restarting a previously completed + printjob (see [#998](https://github.com/foosel/OctoPrint/pull/998)). + * Report an update as failed if the `pip` command returns a return code that + indicates failure. + * Fixed sorting of templates: could only be sorted by name, individual + configurations were ignored (see [#1022](https://github.com/foosel/OctoPrint/pull/1022)). + * Fixed positioning of custom context menus: were offset due to changes in + overall positioning settings (see [#1023](https://github.com/foosel/OctoPrint/pull/1023)). + * Software Update: Don't use display version for comparison of git commit + hashs. + * Fixed temperature parsing for multi extruder setups. + * Fixed nested vertical and horizontal custom control layouts. + +([Commits](https://github.com/foosel/OctoPrint/compare/1.2.4...1.2.5)) + ## 1.2.4 (2015-07-23) ### Improvements diff --git a/README.md b/README.md index 57ebc9abb..06d5b6b04 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,17 @@ OctoPrint ========= -OctoPrint provides a responsive web interface for controlling a 3D printer (RepRap, Ultimaker, ...). It is Free Software +OctoPrint provides a snappy web interface for controlling a 3D printer (RepRap, Ultimaker, ...). It is Free Software and released under the [GNU Affero General Public License V3](http://www.gnu.org/licenses/agpl.html). Its website can be found at [octoprint.org](http://octoprint.org). The documentation is located at [docs.octoprint.org](http://docs.octoprint.org). +The official plugin repository can be reached at [plugins.octoprint.org](http://plugins.octoprint.org). + + + You are currently looking at the source code repository of OctoPrint. If you already installed it (e.g. by using the Raspberry Pi targeted distribution [OctoPi](https://github.com/guysoft/OctoPi)) and only want to find out how to use it, [the documentation](http://docs.octoprint.org/) and [the public wiki](https://github.com/foosel/OctoPrint/wiki) @@ -42,7 +46,8 @@ which is a custom SD card image that includes OctoPrint plus dependencies. After installation, please make sure you follow the first-run wizard and set up access control as necessary. If you want to not only be notified about new releases but also be able to automatically upgrade to them from within -OctoPrint, take a look [at the documentation of the Software Update Plugin](https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update#making-octoprint-updateable-on-existing-installations). +OctoPrint, take a look [at the documentation of the Software Update Plugin](https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update#making-octoprint-updateable-on-existing-installations) +and at its settings. Dependencies ------------ @@ -113,3 +118,4 @@ Special Thanks The development of OctoPrint is sponsored and maintained by [BQ](http://www.bq.com/). Cross-browser testing services are kindly provided by [BrowserStack](http://www.browserstack.com/). +Profiling is done with the help of [PyVmMonitor](http://www.pyvmmonitor.com). diff --git a/docs/configuration/config_yaml.rst b/docs/configuration/config_yaml.rst index 15cfbbbf4..e182e6bb5 100644 --- a/docs/configuration/config_yaml.rst +++ b/docs/configuration/config_yaml.rst @@ -621,7 +621,8 @@ Use the following settings to configure the server: .. code-block:: yaml server: - # Use this option to define the host to which to bind the server, defaults to "0.0.0.0" (= all interfaces) + # Use this option to define the host to which to bind the server, defaults to "0.0.0.0" (= all + # interfaces) host: 0.0.0.0 # Use this option to define the port to which to bind the server, defaults to 5000 @@ -691,6 +692,18 @@ Use the following settings to configure the server: # Maximum size of requests other than file uploads in bytes, defaults to 100KB. maxSize: 102400 + # Commands to restart/shutdown octoprint or the system it's running on + commands: + + # Command to restart OctoPrint, defaults to being unset + serverRestartCommand: sudo service octoprint restart + + # Command to restart the system OctoPrint is running on, defaults to being unset + systemRestartCommand: sudo shutdown -r now + + # Command to shut down the system OctoPrint is running on, defaults to being unset + systemShutdownCommand: sudo shutdown -h now + .. note:: diff --git a/setup.py b/setup.py index 7fff74ecb..5003970e8 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,8 @@ "rsa", "pkginfo", "requests", - "semantic_version" + "semantic_version", + "psutil" ] # Additional requirements for optional install options diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index dae5ed51d..fad1fc41c 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -155,7 +155,11 @@ def register_slicingprogress_callback(self, callback): self._slicing_progress_callbacks.append(callback) def unregister_slicingprogress_callback(self, callback): - self._slicing_progress_callbacks.remove(callback) + try: + self._slicing_progress_callbacks.remove(callback) + except ValueError: + # callback was not registered + pass def _determine_analysis_backlog(self, storage_type, storage_manager): counter = 0 diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index a1cbd8d14..ad1683a1d 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -12,6 +12,7 @@ import os import threading import time +import hashlib from . import version_checks, updaters, exceptions, util @@ -83,8 +84,12 @@ def _load_version_cache(self): self._logger.exception("Error while loading version cache from disk") else: try: - if "octoprint" in data and len(data["octoprint"]) == 4 and "local" in data["octoprint"][1] and "value" in data["octoprint"][1]["local"]: - data_version = data["octoprint"][1]["local"]["value"] + if not isinstance(data, dict): + self._logger.info("Version cache was created in a different format, not using it") + return + + if "__version" in data: + data_version = data["__version"] else: self._logger.info("Can't determine version of OctoPrint version cache was created for, not using it") return @@ -102,24 +107,18 @@ def _load_version_cache(self): self._logger.exception("Error parsing in version cache data") def _save_version_cache(self): - import tempfile import yaml - import shutil + from octoprint.util import atomic_write + from octoprint._version import get_versions - file_obj = tempfile.NamedTemporaryFile(delete=False) - try: + octoprint_version = get_versions()["version"] + self._version_cache["__version"] = octoprint_version + + with atomic_write(self._version_cache_path) as file_obj: yaml.safe_dump(self._version_cache, stream=file_obj, default_flow_style=False, indent=" ", allow_unicode=True) - file_obj.close() - shutil.move(file_obj.name, self._version_cache_path) - self._version_cache_dirty = False - self._logger.info("Saved version cache to disk") - finally: - try: - if os.path.exists(file_obj.name): - os.remove(file_obj.name) - except Exception as e: - self._logger.warn("Could not delete file {}: {}".format(file_obj.name, str(e))) + self._version_cache_dirty = False + self._logger.info("Saved version cache to disk") #~~ SettingsPlugin API @@ -134,9 +133,6 @@ def get_settings_defaults(self): "restart": "octoprint" }, }, - - "octoprint_restart_command": None, - "environment_restart_command": None, "pip_command": None, "cache_ttl": 24 * 60, @@ -146,28 +142,92 @@ def on_settings_load(self): data = dict(octoprint.plugin.SettingsPlugin.on_settings_load(self)) if "checks" in data: del data["checks"] + + checks = self._get_configured_checks() + if "octoprint" in checks: + if "checkout_folder" in checks["octoprint"]: + data["octoprint_checkout_folder"] = checks["octoprint"]["checkout_folder"] + elif "update_folder" in checks["octoprint"]: + data["octoprint_checkout_folder"] = checks["octoprint"]["update_folder"] + else: + data["octoprint_checkout_folder"] = None + data["octoprint_type"] = checks["octoprint"].get("type", None) + else: + data["octoprint_checkout_folder"] = None + data["octoprint_type"] = None + return data def on_settings_save(self, data): for key in self.get_settings_defaults(): - if key == "checks" or key == "cache_ttl": + if key == "checks" or key == "cache_ttl" or key == "octoprint_checkout_folder" or key == "octoprint_type": continue if key in data: self._settings.set([key], data[key]) if "cache_ttl" in data: self._settings.set_int(["cache_ttl"], data["cache_ttl"]) - self._version_cache_ttl = self._settings.get_int(["cache_ttl"]) * 60 + checks = self._get_configured_checks() + if "octoprint" in checks: + check = checks["octoprint"] + update_type = check.get("type", None) + checkout_folder = check.get("checkout_folder", None) + update_folder = check.get("update_folder", None) + + defaults = dict( + plugins=dict(softwareupdate=dict( + checks=dict( + octoprint=dict( + type=update_type, + checkout_folder=checkout_folder, + update_folder=update_folder + ) + ) + )) + ) + + if "octoprint_checkout_folder" in data: + self._settings.set(["checks", "octoprint", "checkout_folder"], data["octoprint_checkout_folder"], defaults=defaults, force=True) + if update_folder and data["octoprint_checkout_folder"]: + self._settings.set(["checks", "octoprint", "update_folder"], None, defaults=defaults, force=True) + self._refresh_configured_checks = True + + if "octoprint_type" in data and data["octoprint_type"] in ("github_release", "git_commit"): + self._settings.set(["checks", "octoprint", "type"], data["octoprint_type"], defaults=defaults, force=True) + self._refresh_configured_checks = True + def get_settings_version(self): - return 3 + return 4 def on_settings_migrate(self, target, current=None): + + if current is None or current < 4: + # config version 4 and higher moves octoprint_restart_command and + # environment_restart_command to the core configuration + + # current plugin commands + configured_octoprint_restart_command = self._settings.get(["octoprint_restart_command"]) + configured_environment_restart_command = self._settings.get(["environment_restart_command"]) + + # current global commands + configured_system_restart_command = self._settings.global_get(["server", "commands", "systemRestartCommand"]) + configured_server_restart_command = self._settings.global_get(["server", "commands", "serverRestartCommand"]) + + # only set global commands if they are not yet set + if configured_system_restart_command is None and configured_environment_restart_command is not None: + self._settings.global_set(["server", "commands", "systemRestartCommand"], configured_environment_restart_command) + if configured_server_restart_command is None and configured_octoprint_restart_command is not None: + self._settings.global_set(["server", "commands", "serverRestartCommand"], configured_octoprint_restart_command) + + # delete current plugin commands from config + self._settings.set(["environment_restart_command"], None) + self._settings.set(["octoprint_restart_command"], None) + if current is None or current == 2: - # 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 + # No config version and config version 2 need the same fix, stripping + # accidentally persisted data off the checks configured_checks = self._settings.get(["checks"], incl_defaults=False) if configured_checks is None: @@ -213,6 +273,10 @@ def on_settings_migrate(self, target, current=None): self._settings.set(["checks", key], None, defaults=dummy_defaults) elif current == 1: + # config version 1 had the error that the octoprint check got accidentally + # included in checks["octoprint"], leading to recursion and hence to + # yaml parser errors + configured_checks = self._settings.get(["checks"], incl_defaults=False) if configured_checks is None: return @@ -223,7 +287,6 @@ def on_settings_migrate(self, target, current=None): dummy_defaults["plugins"][self._identifier] = dict(checks=dict()) dummy_defaults["plugins"][self._identifier]["checks"]["octoprint"] = None self._settings.set(["checks", "octoprint"], None, defaults=dummy_defaults) - self._settings.save() def _clean_settings_check(self, key, data, defaults, delete=None, save=True): if delete is None: @@ -337,10 +400,12 @@ def get_current_versions(self, check_targets=None, force=False): if not target in check_targets: continue + populated_check = self._populated_check(target, check) + try: - target_information, target_update_available, target_update_possible = self._get_current_version(target, 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: - continue + target_information = dict() except exceptions.UnknownCheckType: self._logger.warn("Unknown update check type for %s" % target) continue @@ -349,32 +414,39 @@ def get_current_versions(self, check_targets=None, force=False): update_available = update_available or target_update_available update_possible = update_possible or (target_update_possible and target_update_available) - information[target] = dict(updateAvailable=target_update_available, updatePossible=target_update_possible, information=target_information) - if "displayName" in check: - information[target]["displayName"] = check["displayName"] + from octoprint._version import get_versions + octoprint_version = get_versions()["version"] + local_name = target_information["local"]["name"] + local_value = target_information["local"]["value"] - if "displayVersion" in check: - from octoprint._version import get_versions - octoprint_version = get_versions()["version"] - local_name = target_information["local"]["name"] - local_value = target_information["local"]["value"] - information[target]["displayVersion"] = check["displayVersion"].format(octoprint_version=octoprint_version, local_name=local_name, local_value=local_value) + information[target] = dict(updateAvailable=target_update_available, + updatePossible=target_update_possible, + information=target_information, + displayName=populated_check["displayName"], + displayVersion=populated_check["displayVersion"].format(octoprint_version=octoprint_version, local_name=local_name, local_value=local_value), + check=populated_check) if self._version_cache_dirty: self._save_version_cache() return information, update_available, update_possible + def _get_check_hash(self, check): + hash = hashlib.md5() + hash.update(repr(check)) + return hash.hexdigest() + def _get_current_version(self, target, check, force=False): """ Determines the current version information for one target based on its check configuration. """ + current_hash = self._get_check_hash(check) if target in self._version_cache and not force: - timestamp, information, update_available, update_possible = self._version_cache[target] - if timestamp + self._version_cache_ttl >= time.time() > timestamp: + data = self._version_cache[target] + if data["hash"] == current_hash and data["timestamp"] + self._version_cache_ttl >= time.time() > data["timestamp"]: # we also check that timestamp < now to not get confused too much by clock changes - return information, update_available, update_possible + return data["information"], data["available"], data["possible"] information = dict() update_available = False @@ -397,7 +469,11 @@ def _get_current_version(self, target, check, force=False): except: update_possible = False - self._version_cache[target] = (time.time(), information, update_available, update_possible) + self._version_cache[target] = dict(timestamp=time.time(), + hash=current_hash, + information=information, + available=update_available, + possible=update_possible) self._version_cache_dirty = True return information, update_available, update_possible @@ -481,7 +557,11 @@ def _update_worker(self, checks, check_targets, force): if restart_type is not None and restart_type in ("octoprint", "environment"): # one of our updates requires a restart of either type "octoprint" or "environment". Let's see if # we can actually perform that - restart_command = self._settings.get(["%s_restart_command" % restart_type]) + + if restart_type == "octoprint": + restart_command = self._settings.global_get(["server", "commands", "serverRestartCommand"]) + elif restart_type == "environment": + restart_command = self._settings.global_get(["server", "commands", "systemRestartCommand"]) if restart_command is not None: self._send_client_message("restarting", dict(restart_type=restart_type, results=target_results)) @@ -574,6 +654,30 @@ def _perform_restart(self, restart_command): self._logger.warn("Restart stderr:\n%s" % e.stderr) raise exceptions.RestartFailed() + def _populated_check(self, target, check): + result = dict(check) + + if target == "octoprint": + from flask.ext.babel import gettext + result["displayName"] = check.get("displayName", gettext("OctoPrint")) + result["displayVersion"] = check.get("displayVersion", "{octoprint_version}") + + from octoprint._version import get_versions + versions = get_versions() + if check["type"] == "github_commit": + result["current"] = versions.get("full-revisionid", versions.get("full", "unknown")) + else: + result["current"] = versions["version"] + else: + result["displayName"] = check.get("displayName", target) + result["displayVersion"] = check.get("displayVersion", check.get("current", "unknown")) + if check["type"] in ("github_commit"): + result["current"] = check.get("current", None) + else: + result["current"] = check.get("current", check.get("displayVersion", None)) + + return result + def _get_version_checker(self, target, check): """ Retrieves the version checker to use for given target and check configuration. Will raise an UnknownCheckType diff --git a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js index dbd0579e8..fd4ba14ff 100644 --- a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js +++ b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js @@ -13,12 +13,20 @@ $(function() { self.currentlyBeingUpdated = []; - self.config_restartCommand = ko.observable(); - self.config_rebootCommand = ko.observable(); + self.octoprintUnconfigured = ko.observable(); + self.octoprintUnreleased = ko.observable(); + self.config_cacheTtl = ko.observable(); + self.config_checkoutFolder = ko.observable(); + self.config_checkType = ko.observable(); self.configurationDialog = $("#settings_plugin_softwareupdate_configurationdialog"); + self.config_availableCheckTypes = [ + {"key": "github_release", "name": gettext("Release")}, + {"key": "git_commit", "name": gettext("Commit")} + ]; + self.versions = new ItemListHelper( "plugin.softwareupdate.versions", { @@ -78,19 +86,23 @@ $(function() { var data = { plugins: { softwareupdate: { - octoprint_restart_command: self.config_restartCommand(), - environment_restart_command: self.config_rebootCommand(), - cache_ttl: parseInt(self.config_cacheTtl()) + cache_ttl: parseInt(self.config_cacheTtl()), + octoprint_checkout_folder: self.config_checkoutFolder(), + octoprint_type: self.config_checkType() } } }; - self.settings.saveData(data, function() { self.configurationDialog.modal("hide"); self._copyConfig(); }); + self.settings.saveData(data, function() { + self.configurationDialog.modal("hide"); + self._copyConfig(); + self.performCheck(); + }); }; self._copyConfig = function() { - self.config_restartCommand(self.settings.settings.plugins.softwareupdate.octoprint_restart_command()); - self.config_rebootCommand(self.settings.settings.plugins.softwareupdate.environment_restart_command()); self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl()); + self.config_checkoutFolder(self.settings.settings.plugins.softwareupdate.octoprint_checkout_folder()); + self.config_checkType(self.settings.settings.plugins.softwareupdate.octoprint_type()); }; self.fromCheckResponse = function(data, ignoreSeen, showIfNothingNew) { @@ -109,6 +121,25 @@ $(function() { }); self.versions.updateItems(versions); + var octoprint = data.information["octoprint"]; + if (octoprint && octoprint.hasOwnProperty("check")) { + var check = octoprint.check; + if (BRANCH != "master" && check["type"] == "github_release") { + self.octoprintUnreleased(true); + } else { + self.octoprintUnreleased(false); + } + + var checkoutFolder = (check["checkout_folder"] || "").trim(); + var updateFolder = (check["update_folder"] || "").trim(); + var checkType = check["type"] || ""; + if ((checkType == "github_release" || checkType == "git_commit") && checkoutFolder == "" && updateFolder == "") { + self.octoprintUnconfigured(true); + } else { + self.octoprintUnconfigured(false); + } + } + if (data.status == "updateAvailable" || data.status == "updatePossible") { var text = gettext("There are updates available for the following components:"); diff --git a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 index 96f017131..c3b0bdc8a 100644 --- a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 +++ b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 @@ -1,3 +1,21 @@ +
+ You are running a non-release version of OctoPrint but are tracking OctoPrint + releases. +
+ You probably want OctoPrint to track the matching development version instead. + If you have a local OctoPrint checkout folder switched to another branch, + simply switching over to "Commit" tracking will already + take care of that. Otherwise please take a look at the + Documentation. +
+{% endtrans %}