Permalink
Comparing changes
Open a pull request
- 14 commits
- 17 files changed
- 0 commit comments
- 1 contributor
Commits on Jun 25, 2015
Commits on Jun 28, 2015
Custom exception should be derived from Exception, not BaseException. Not only is this specified in the python documentation, but also tornado will be able to handle Exceptions in requests perfectly fine and return an HTTP 500 for them, but crash hard (as in, server shut down follows) for BaseExceptions. (cherry picked from commit 29b047b)
Commits on Jun 29, 2015
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 all versions that follow too). 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 and up but not 1.3 or anything above that. (cherry picked from commit d5692d7)
Local file storage implementation will now "touch" added files to set modification date to current date, ensuring that files that are just moved through a custom implementation of file object (e.g. through the watched folder mechanism) will show with "now" as the upload date. (cherry picked from commit 5f0c89c)
Plugin Manager now internally uses pkg_resources.parse_version and pkg_resources.parse_requirements to check for version compatibility with plugins from the repository. That will allow to directly use the OctoPrint version string once it's PEP440 compatible. (cherry picked from commit 8eb61a9)
Unified
Split
Showing
with
156 additions
and 48 deletions.
- +7 −6 .versioneer-lookup
- +26 −0 CHANGELOG.md
- +21 −0 docs/configuration/config_yaml.rst
- +14 −13 src/octoprint/events.py
- +3 −0 src/octoprint/filemanager/storage.py
- +4 −4 src/octoprint/plugin/core.py
- +41 −9 src/octoprint/plugins/pluginmanager/__init__.py
- +5 −1 src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js
- +1 −1 src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2
- +1 −1 src/octoprint/printer/__init__.py
- +14 −2 src/octoprint/server/__init__.py
- +1 −1 src/octoprint/server/api/languages.py
- +2 −2 src/octoprint/server/api/slicing.py
- +3 −3 src/octoprint/slicing/exceptions.py
- +5 −1 src/octoprint/static/js/app/viewmodels/files.js
- +1 −1 src/octoprint/templates/sidebar/files.jinja2
- +7 −3 src/octoprint/util/comm.py
| @@ -7,12 +7,13 @@ | ||
| # The file is processed from top to bottom, the first matching line wins. If <tag> or <reference commit> 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 | ||
| @@ -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 | ||
| @@ -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 | ||
| @@ -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,27 +276,28 @@ 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 = [] | ||
| for c in command: | ||
| 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): | ||
| @@ -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): | ||
| @@ -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 | ||
|
|
||
| @@ -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 | ||
| } | ||
| @@ -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) { | ||
| @@ -83,7 +83,7 @@ | ||
| </a> | ||
| </h4> | ||
|
|
||
| <form class="form-search"> | ||
| <form class="form-search" data-bind="submit: performRepositorySearch"> | ||
| <input type="text" class="input-block search-query" data-bind="value: repositorySearchQuery, valueUpdate: 'input'" placeholder="{{ _('Search...') }}"> | ||
| </form> | ||
|
|
||
| @@ -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 | ||
| @@ -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!") | ||
| @@ -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 | ||
| @@ -57,7 +57,7 @@ def slicingListSlicerProfiles(slicer): | ||
| @api.route("/slicing/<string:slicer>/profiles/<string:name>", 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: | ||
| @@ -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 | ||
|
|
||
| @@ -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) { | ||
| @@ -1,4 +1,4 @@ | ||
| <form class="form-search"> | ||
| <form class="form-search" data-bind="submit: performSearch"> | ||
| <input type="text" class="input-block search-query" data-bind="value: searchQuery, valueUpdate: 'input'" placeholder="{{ _('Search...') }}"> | ||
| </form> | ||
| <div class="gcode_files" data-bind="slimScrolledForeach: listHelper.paginatedItems"> | ||
Oops, something went wrong.