diff --git a/.versioneer-lookup b/.versioneer-lookup index 8e1218d79..0e9ab0a09 100644 --- a/.versioneer-lookup +++ b/.versioneer-lookup @@ -7,12 +7,13 @@ # The file is processed from top to bottom, the first matching line wins. If or are left out, # the lookup table does not apply to the matched branches -# master and staging shall not use the lookup table +# master shall not use the lookup table, only tags master -staging -# fix/ branches are fixes for master, so we don't handle those either -fix/.* +# maintenance is currently the branch for preparation of maintenance release 1.2.1 +# so are any fix/... branches +maintenance 1.2.1-dev cfa4cb2a7c5f1af10dc8 +fix/.* 1.2.1-dev cfa4cb2a7c5f1af10dc8 -# every other branch is a development branch and thus gets resolved to 1.2.0-dev for now -.* 1.2.0-dev 50cf776e70b9 +# 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/CHANGELOG.md b/CHANGELOG.md index fe57f3e8f..b565dcb17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # OctoPrint Changelog +## 1.2.1 (2015-06-30) + +### Improvements + +* More flexibility when interpreting compatibility data from plugin repository. If compatibility information is provided + only as a version number it's prefixed with `>=` for the check (so stating a compatibility of only + `1.2.0` will now make the plugin compatible to OctoPrint 1.2.0+, not only 1.2.0). Alternatively the compatibility + information may now contain stuff like `>=1.2,<1.3` in which case the plugin will only be shown as compatible + to OctoPrint versions 1.2.0 and up but not 1.3.0 or anything above that. See also + [the requirement specification format of the `pkg_resources` package](https://pythonhosted.org/setuptools/pkg_resources.html#requirements-parsing). +* Only print the commands of configured event handlers to the log when a new `debug` flag is present in the config + (see [the docs](http://docs.octoprint.org/en/master/configuration/config_yaml.html#events)). Reduces risk of disclosing sensitive data when sharing log files. + +### Bug Fixes + +* [#956](https://github.com/foosel/OctoPrint/issues/956) - Fixed server crash when trying to configure a default + slicing profile for a still unconfigured slicer. +* [#957](https://github.com/foosel/OctoPrint/issues/957) - Increased maximum allowed body size for plugin archive uploads. +* Bugs without tickets: + * Clean exit on `SIGTERM`, calling the shutdown functions provided by plugins. + * Don't disconnect on `volume.init` errors from the firmware. + * `touch` uploaded files on local file storage to ensure proper "uploaded date" even for files that are just moved + from other locations of the file system (e.g. when being added from the `watched` folder). + +([Commits](https://github.com/foosel/OctoPrint/compare/1.2.0...1.2.1)) + ## 1.2.0 (2015-06-25) ### Note for Upgraders diff --git a/docs/configuration/config_yaml.rst b/docs/configuration/config_yaml.rst index f698a3ba8..15cfbbbf4 100644 --- a/docs/configuration/config_yaml.rst +++ b/docs/configuration/config_yaml.rst @@ -366,6 +366,27 @@ Use the following settings to add shell/gcode commands to be executed on certain type: gcode enabled: False +.. note:: + + For debugging purposes, you can also add an additional property ``debug`` to your event subscription definitions + that if set to true will make the event handler print a log line with your subscription's command after performing + all placeholder replacements. Example: + + .. code-block:: yaml + + events: + subscriptions: + - event: Startup + command: "logger 'OctoPrint started up'" + type: system + debug: true + + This will be logged in OctoPrint's logfile as + + .. code-block:: none + + Executing System Command: logger 'OctoPrint started up' + .. _sec-configuration-config_yaml-feature: Feature diff --git a/src/octoprint/events.py b/src/octoprint/events.py index cbcbf3b82..a26a48e8f 100644 --- a/src/octoprint/events.py +++ b/src/octoprint/events.py @@ -254,10 +254,11 @@ def _initSubscriptions(self): event = subscription["event"] command = subscription["command"] commandType = subscription["type"] + debug = subscription["debug"] if "debug" in subscription else False if not event in self._subscriptions.keys(): self._subscriptions[event] = [] - self._subscriptions[event].append((command, commandType)) + self._subscriptions[event].append((command, commandType, debug)) if not event in eventsToSubscribe: eventsToSubscribe.append(event) @@ -275,7 +276,7 @@ def eventCallback(self, event, payload): if not event in self._subscriptions: return - for command, commandType in self._subscriptions[event]: + for command, commandType, debug in self._subscriptions[event]: try: if isinstance(command, (tuple, list, set)): processedCommand = [] @@ -283,19 +284,20 @@ def eventCallback(self, event, payload): processedCommand.append(self._processCommand(c, payload)) else: processedCommand = self._processCommand(command, payload) - self.executeCommand(processedCommand, commandType) + self.executeCommand(processedCommand, commandType, debug=debug) except KeyError, e: self._logger.warn("There was an error processing one or more placeholders in the following command: %s" % command) - def executeCommand(self, command, commandType): + def executeCommand(self, command, commandType, debug=False): if commandType == "system": - self._executeSystemCommand(command) + self._executeSystemCommand(command, debug=debug) elif commandType == "gcode": - self._executeGcodeCommand(command) + self._executeGcodeCommand(command, debug=debug) - def _executeSystemCommand(self, command): + def _executeSystemCommand(self, command, debug=False): def commandExecutioner(command): - self._logger.info("Executing system command: %s" % command) + if debug: + self._logger.info("Executing system command: %s" % command) subprocess.Popen(command, shell=True) try: @@ -306,16 +308,15 @@ def commandExecutioner(command): commandExecutioner(command) except subprocess.CalledProcessError, e: self._logger.warn("Command failed with return code %i: %s" % (e.returncode, str(e))) - except Exception, ex: + except: self._logger.exception("Command failed") - def _executeGcodeCommand(self, command): + def _executeGcodeCommand(self, command, debug=False): commands = [command] if isinstance(command, (list, tuple, set)): - self._logger.debug("Executing GCode commands: %r" % command) commands = list(command) - else: - self._logger.debug("Executing GCode command: %s" % command) + if debug: + self._logger.info("Executing GCode commands: %r" % command) self._printer.commands(commands) def _processCommand(self, command, payload): diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index 6e855cce6..9f238cf93 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -455,6 +455,9 @@ def add_file(self, path, file_object, printer_profile=None, links=None, allow_ov self._add_links(name, path, links) + # touch the file to set last access and modification time to now + os.utime(file_path, None) + return self.path_in_storage((path, name)) def remove_file(self, path): diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 8a5053724..913289de8 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -1213,15 +1213,15 @@ def on_plugin_disabled(self): class RestartNeedingPlugin(Plugin): pass -class PluginNeedsRestart(BaseException): +class PluginNeedsRestart(Exception): def __init__(self, name): - BaseException.__init__(self) + Exception.__init__(self) self.name = name self.message = "Plugin {name} cannot be enabled or disabled after system startup".format(**locals()) -class PluginLifecycleException(BaseException): +class PluginLifecycleException(Exception): def __init__(self, name, reason, message): - BaseException.__init__(self) + Exception.__init__(self) self.name = name self.reason = reason diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index 152d9b415..6c4e039b0 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -55,6 +55,12 @@ def initialize(self): self._pip_caller.on_log_stdout = self._log_stdout self._pip_caller.on_log_stderr = self._log_stderr + ##~~ Body size hook + + def increase_upload_bodysize(self, current_max_body_sizes, *args, **kwargs): + # set a maximum body size of 50 MB for plugin archive uploads + return [("POST", r"/upload_archive", 50 * 1024 * 1024)] + ##~~ StartupPlugin def on_startup(self, host, port): @@ -510,22 +516,40 @@ def map_repository_entry(entry): if "compatibility" in entry: if "octoprint" in entry["compatibility"] and entry["compatibility"]["octoprint"] is not None and len(entry["compatibility"]["octoprint"]): - import semantic_version - for octo_compat in entry["compatibility"]["octoprint"]: - s = semantic_version.Spec("=={}".format(octo_compat)) - if semantic_version.Version(octoprint_version) in s: - break - else: - result["is_compatible"]["octoprint"] = False + result["is_compatible"]["octoprint"] = self._is_octoprint_compatible(octoprint_version, entry["compatibility"]["octoprint"]) if "os" in entry["compatibility"] and entry["compatibility"]["os"] is not None and len(entry["compatibility"]["os"]): - result["is_compatible"]["os"] = current_os in entry["compatibility"]["os"] + result["is_compatible"]["os"] = self._is_os_compatible(current_os, entry["compatibility"]["os"]) return result self._repository_plugins = map(map_repository_entry, repo_data) return True + def _is_octoprint_compatible(self, octoprint_version_string, compatibility_entries): + """ + Tests if the current ``octoprint_version`` is compatible to any of the provided ``compatibility_entries``. + """ + + octoprint_version = pkg_resources.parse_version(octoprint_version_string) + for octo_compat in compatibility_entries: + if not any(octo_compat.startswith(c) for c in ("<", "<=", "!=", "==", ">=", ">", "~=", "===")): + octo_compat = ">={}".format(octo_compat) + + s = next(pkg_resources.parse_requirements("OctoPrint" + octo_compat)) + if octoprint_version in s: + break + else: + return False + + return True + + def _is_os_compatible(self, current_os, compatibility_entries): + """ + Tests if the ``current_os`` matches any of the provided ``compatibility_entries``. + """ + return current_os in compatibility_entries + def _get_os(self): if sys.platform == "win32": return "windows" @@ -562,4 +586,12 @@ def _to_external_representation(self, plugin): __plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Plugin-Manager" __plugin_description__ = "Allows installing and managing OctoPrint plugins" __plugin_license__ = "AGPLv3" -__plugin_implementation__ = PluginManagerPlugin() + +def __plugin_load__(): + global __plugin_implementation__ + __plugin_implementation__ = PluginManagerPlugin() + + global __plugin_hooks__ + __plugin_hooks__ = { + "octoprint.server.http.bodysize": __plugin_implementation__.increase_upload_bodysize + } diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 04aa302c4..8d0fb4177 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -147,7 +147,11 @@ $(function() { } }); - self.performRepositorySearch = function() { + self.performRepositorySearch = function(e) { + if (e !== undefined) { + e.preventDefault(); + } + var query = self.repositorySearchQuery(); if (query !== undefined && query.trim() != "") { self.repositoryplugins.changeSearchFunction(function(entry) { diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index bc05691e6..559817c06 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -83,7 +83,7 @@ - diff --git a/src/octoprint/printer/__init__.py b/src/octoprint/printer/__init__.py index e0989a936..0a4195da4 100644 --- a/src/octoprint/printer/__init__.py +++ b/src/octoprint/printer/__init__.py @@ -467,6 +467,6 @@ def on_printer_send_current_data(self, data): """ pass -class UnknownScript(BaseException): +class UnknownScript(Exception): def __init__(self, name, *args, **kwargs): self.name = name diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 8426628fd..35586d1dd 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -20,6 +20,7 @@ import logging import logging.config import atexit +import signal SUCCESS = {} NO_CONTENT = ("", 204) @@ -440,16 +441,27 @@ def call_on_after_startup(name, plugin): # prepare our shutdown function def on_shutdown(): - self._logger.info("Goodbye!") + # will be called on clean system exit and shutdown the watchdog observer and call the on_shutdown methods + # on all registered ShutdownPlugins + self._logger.info("Shutting down...") observer.stop() observer.join() octoprint.plugin.call_plugin(octoprint.plugin.ShutdownPlugin, "on_shutdown") + self._logger.info("Goodbye!") atexit.register(on_shutdown) + def sigterm_handler(*args, **kwargs): + # will stop tornado on SIGTERM, making the program exit cleanly + def shutdown_tornado(): + ioloop.stop() + ioloop.add_callback_from_signal(shutdown_tornado) + signal.signal(signal.SIGTERM, sigterm_handler) + try: + # this is the main loop - as long as tornado is running, OctoPrint is running ioloop.start() - except KeyboardInterrupt: + except (KeyboardInterrupt, SystemExit): pass except: self._logger.fatal("Now that is embarrassing... Something really really went wrong here. Please report this including the stacktrace below in OctoPrint's bugtracker. Thanks!") diff --git a/src/octoprint/server/api/languages.py b/src/octoprint/server/api/languages.py index a3690f741..820089e47 100644 --- a/src/octoprint/server/api/languages.py +++ b/src/octoprint/server/api/languages.py @@ -143,5 +143,5 @@ def _validate_archive_name(name): raise InvalidLanguagePack("Provided language pack contains invalid name {name}".format(**locals())) -class InvalidLanguagePack(BaseException): +class InvalidLanguagePack(Exception): pass diff --git a/src/octoprint/server/api/slicing.py b/src/octoprint/server/api/slicing.py index d2b254667..7dd80f3bc 100644 --- a/src/octoprint/server/api/slicing.py +++ b/src/octoprint/server/api/slicing.py @@ -57,7 +57,7 @@ def slicingListSlicerProfiles(slicer): @api.route("/slicing//profiles/", methods=["GET"]) def slicingGetSlicerProfile(slicer, name): try: - profile = slicingManager.load_profile(slicer, name) + profile = slicingManager.load_profile(slicer, name, require_configured=False) except UnknownSlicer: return make_response("Unknown slicer {slicer}".format(**locals()), 404) except UnknownProfile: @@ -106,7 +106,7 @@ def slicingPatchSlicerProfile(slicer, name): return make_response("Expected content-type JSON", 400) try: - profile = slicingManager.load_profile(slicer, name) + profile = slicingManager.load_profile(slicer, name, require_configured=False) except UnknownSlicer: return make_response("Unknown slicer {slicer}".format(**locals()), 404) except UnknownProfile: diff --git a/src/octoprint/slicing/exceptions.py b/src/octoprint/slicing/exceptions.py index 4d7891073..8a543e9fa 100644 --- a/src/octoprint/slicing/exceptions.py +++ b/src/octoprint/slicing/exceptions.py @@ -33,7 +33,7 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License" -class SlicingException(BaseException): +class SlicingException(Exception): """ Base exception of all slicing related exceptions. """ @@ -73,7 +73,7 @@ def __init__(self, slicer, *args, **kwargs): SlicerException.__init__(self, slicer, *args, **kwargs) self.message = "No such slicer: {slicer}".format(slicer=slicer) -class ProfileException(BaseException): +class ProfileException(Exception): """ Base exception of all slicing profile related exceptions. @@ -86,7 +86,7 @@ class ProfileException(BaseException): Identifier of the profile for which the exception was raised. """ def __init__(self, slicer, profile, *args, **kwargs): - BaseException.__init__(self, *args, **kwargs) + Exception.__init__(self, *args, **kwargs) self.slicer = slicer self.profile = profile diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 49c2666bb..a5d50f8ba 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -308,7 +308,11 @@ $(function() { return output; }; - self.performSearch = function() { + self.performSearch = function(e) { + if (e !== undefined) { + e.preventDefault(); + } + var query = self.searchQuery(); if (query !== undefined && query.trim() != "") { self.listHelper.changeSearchFunction(function(entry) { diff --git a/src/octoprint/templates/sidebar/files.jinja2 b/src/octoprint/templates/sidebar/files.jinja2 index 50067e036..66adac4a6 100644 --- a/src/octoprint/templates/sidebar/files.jinja2 +++ b/src/octoprint/templates/sidebar/files.jinja2 @@ -1,4 +1,4 @@ -
diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 14ca4f594..8dd693327 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -1298,10 +1298,14 @@ def _handleErrors(self, line): if self._regex_minMaxError.match(line): line = line.rstrip() + self._readline() - #Skip the communication errors, as those get corrected. if 'line number' in line.lower() or 'checksum' in line.lower() or 'expected line' in line.lower(): + #Skip the communication errors, as those get corrected. self._lastCommError = line[6:] if line.startswith("Error:") else line[2:] pass + elif 'volume.init' in line.lower() or "openroot" in line.lower() or 'workdir' in line.lower()\ + or "error writing to file" in line.lower(): + #Also skip errors with the SD card + pass elif not self.isError(): self._errorValue = line[6:] if line.startswith("Error:") else line[2:] self._changeState(self.STATE_ERROR) @@ -1982,9 +1986,9 @@ def _get(self): return item -class TypeAlreadyInQueue(BaseException): +class TypeAlreadyInQueue(Exception): def __init__(self, t, *args, **kwargs): - BaseException.__init__(self, *args, **kwargs) + Exception.__init__(self, *args, **kwargs) self.type = t