From 7b3e0563cc84d90d9fbdb536630b92fc3b6b38a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 1 Jul 2015 09:19:51 +0200 Subject: [PATCH 1/8] SWUpdate: Only use version cache from same version of OP When using the version cache only use the version cache if the OctoPrint version stored within it matches the one of the currently running instance. Otherwise we might report false positives with regards to available updates under some circumstances. (cherry picked from commit bb7b0cb) --- .../plugins/softwareupdate/__init__.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index e6972737..7cfdd2f3 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -82,9 +82,24 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, except: self._logger.exception("Error while loading version cache from disk") else: - self._version_cache = data - self._version_cache_dirty = False - self._logger.info("Loaded version cache from disk") + 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"] + else: + self._logger.info("Can't determine version of OctoPrint version cache was created for, not using it") + return + + from octoprint._version import get_versions + octoprint_version = get_versions()["version"] + if data_version != octoprint_version: + self._logger.info("Version cache was created for another version of OctoPrint, not using it") + return + + self._version_cache = data + self._version_cache_dirty = False + self._logger.info("Loaded version cache from disk") + except: + self._logger.exception("Error parsing in version cache data") def _save_version_cache(self): import tempfile From d7a86a4d28ea4b31132f7938b415eb1dea926aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 2 Jul 2015 08:36:03 +0200 Subject: [PATCH 2/8] Use UTF-8 for _all_ output from sarge Lines taking from the asynchronous processing of stdout/stderr where left as str, leading to encoding problems when utf8 characters showed up in the stream and were being interpreted as ascii encoding. (cherry picked from commit 9373be3) --- src/octoprint/plugins/cura/__init__.py | 46 +++++++++---------- .../plugins/pluginmanager/__init__.py | 6 +-- .../plugins/softwareupdate/updaters/pip.py | 10 ++-- src/octoprint/util/__init__.py | 22 +++++++-- src/octoprint/util/pip.py | 5 ++ 5 files changed, 54 insertions(+), 35 deletions(-) diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py index e1ce090c..6b1b7291 100644 --- a/src/octoprint/plugins/cura/__init__.py +++ b/src/octoprint/plugins/cura/__init__.py @@ -218,7 +218,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, if not on_progress_kwargs: on_progress_kwargs = dict() - self._cura_logger.info("### Slicing %s to %s using profile stored at %s" % (model_path, machinecode_path, profile_path)) + self._cura_logger.info(u"### Slicing %s to %s using profile stored at %s" % (model_path, machinecode_path, profile_path)) engine_settings = self._convert_to_engine(profile_path, printer_profile, posX, posY) @@ -227,16 +227,15 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, return False, "Path to CuraEngine is not configured " working_dir, _ = os.path.split(executable) - args = ['"%s"' % executable, '-v', '-p'] + args = [executable, '-v', '-p'] for k, v in engine_settings.items(): - args += ["-s", '"%s=%s"' % (k, str(v))] - args += ['-o', '"%s"' % machinecode_path, '"%s"' % model_path] + args += ["-s", "%s=%s" % (k, str(v))] + args += ["-o", machinecode_path, model_path] + + self._logger.info(u"Running %r in %s" % (" ".join(args), working_dir)) import sarge - command = " ".join(args) - self._logger.info("Running %r in %s" % (command, working_dir)) - - p = sarge.run(command, cwd=working_dir, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) + p = sarge.run(args, cwd=working_dir, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) p.wait_events() self._slicing_commands[machinecode_path] = p.commands[0] @@ -254,6 +253,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, p.commands[0].poll() continue + line = octoprint.util.to_unicode(line, errors="replace") self._cura_logger.debug(line.strip()) if on_progress is not None: @@ -282,14 +282,14 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, # # with being 0 for "inset", 1 for "skin" and 2 for "export". - if line.startswith("Layer count:") and layer_count is None: + if line.startswith(u"Layer count:") and layer_count is None: try: - layer_count = float(line[len("Layer count:"):].strip()) + layer_count = float(line[len(u"Layer count:"):].strip()) except: pass - elif line.startswith("Progress:"): - split_line = line[len("Progress:"):].strip().split(":") + elif line.startswith(u"Progress:"): + split_line = line[len(u"Progress:"):].strip().split(":") if len(split_line) == 3: step, current_layer, _ = split_line try: @@ -302,21 +302,21 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, on_progress_kwargs["_progress"] = (step_factor[step] * layer_count + current_layer) / (layer_count * 3) on_progress(*on_progress_args, **on_progress_kwargs) - elif line.startswith("Print time:"): + elif line.startswith(u"Print time:"): try: - print_time = int(line[len("Print time:"):].strip()) + print_time = int(line[len(u"Print time:"):].strip()) if analysis is None: analysis = dict() analysis["estimatedPrintTime"] = print_time except: pass - elif line.startswith("Filament:") or line.startswith("Filament2:"): - if line.startswith("Filament:"): - filament_str = line[len("Filament:"):].strip() + elif line.startswith(u"Filament:") or line.startswith(u"Filament2:"): + if line.startswith(u"Filament:"): + filament_str = line[len(u"Filament:"):].strip() tool_key = "tool0" else: - filament_str = line[len("Filament2:"):].strip() + filament_str = line[len(u"Filament2:"):].strip() tool_key = "tool1" try: @@ -339,20 +339,20 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, with self._job_mutex: if machinecode_path in self._cancelled_jobs: - self._cura_logger.info("### Cancelled") + self._cura_logger.info(u"### Cancelled") raise octoprint.slicing.SlicingCancelled() - self._cura_logger.info("### Finished, returncode %d" % p.returncode) + self._cura_logger.info(u"### Finished, returncode %d" % p.returncode) if p.returncode == 0: return True, dict(analysis=analysis) else: - self._logger.warn("Could not slice via Cura, got return code %r" % p.returncode) + self._logger.warn(u"Could not slice via Cura, got return code %r" % p.returncode) return False, "Got returncode %r" % p.returncode except octoprint.slicing.SlicingCancelled as e: raise e except: - self._logger.exception("Could not slice via Cura, got an unknown error") + self._logger.exception(u"Could not slice via Cura, got an unknown error") return False, "Unknown error, please consult the log file" finally: @@ -371,7 +371,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, command = self._slicing_commands[machinecode_path] if command is not None: command.terminate() - self._logger.info("Cancelled slicing of %s" % machinecode_path) + self._logger.info(u"Cancelled slicing of %s" % machinecode_path) def _load_profile(self, path): import yaml diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index 58b662bb..a458acd1 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -418,13 +418,13 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, return self._pip_caller.execute(*args) def _log_call(self, *lines): - self._log(lines, prefix=" ", stream="call") + self._log(lines, prefix=u" ", stream="call") def _log_stdout(self, *lines): - self._log(lines, prefix=">", stream="stdout") + self._log(lines, prefix=u">", stream="stdout") def _log_stderr(self, *lines): - self._log(lines, prefix="!", stream="stderr") + self._log(lines, prefix=u"!", stream="stderr") def _log(self, lines, prefix=None, stream=None, strip=True): if strip: diff --git a/src/octoprint/plugins/softwareupdate/updaters/pip.py b/src/octoprint/plugins/softwareupdate/updaters/pip.py index 36f52812..446313b0 100644 --- a/src/octoprint/plugins/softwareupdate/updaters/pip.py +++ b/src/octoprint/plugins/softwareupdate/updaters/pip.py @@ -46,12 +46,12 @@ def perform_update(target, check, target_version): install_arg = check["pip"].format(target_version=target_version) - logger.debug("Target: %s, executing pip install %s" % (target, install_arg)) + logger.debug(u"Target: %s, executing pip install %s" % (target, install_arg)) pip_args = ["install", check["pip"].format(target_version=target_version, target=target_version)] pip_caller.execute(*pip_args) - logger.debug("Target: %s, executing pip install %s --ignore-reinstalled --force-reinstall --no-deps" % (target, install_arg)) + logger.debug(u"Target: %s, executing pip install %s --ignore-reinstalled --force-reinstall --no-deps" % (target, install_arg)) pip_args += ["--ignore-installed", "--force-reinstall", "--no-deps"] pip_caller.execute(*pip_args) @@ -59,13 +59,13 @@ def perform_update(target, check, target_version): return "ok" def _log_call(*lines): - _log(lines, prefix=" ") + _log(lines, prefix=u" ") def _log_stdout(*lines): - _log(lines, prefix=">") + _log(lines, prefix=u">") def _log_stderr(*lines): - _log(lines, prefix="!") + _log(lines, prefix=u"!") def _log(lines, prefix=None): lines = map(lambda x: x.strip(), lines) diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py index 9b63618e..3b3d03a4 100644 --- a/src/octoprint/util/__init__.py +++ b/src/octoprint/util/__init__.py @@ -352,9 +352,7 @@ def silent_remove(file): def sanitize_ascii(line): if not isinstance(line, basestring): raise ValueError("Expected either str or unicode but got {} instead".format(line.__class__.__name__ if line is not None else None)) - if isinstance(line, str): - line = unicode(line, 'ascii', 'replace') - return line.encode('ascii', 'replace').rstrip() + return to_unicode(line, encoding="ascii", errors="replace").rstrip() def filter_non_ascii(line): @@ -369,12 +367,28 @@ def filter_non_ascii(line): """ try: - unicode(line, 'ascii').encode('ascii') + to_str(to_unicode(line, encoding="ascii"), encoding="ascii") return False except ValueError: return True +def to_str(s_or_u, encoding="utf-8", errors="strict"): + """Make sure ``s_or_u`` is a str.""" + if isinstance(s_or_u, unicode): + return s_or_u.encode(encoding, errors=errors) + else: + return s_or_u + + +def to_unicode(s_or_u, encoding="utf-8", errors="strict"): + """Make sure ``s_or_u`` is a unicode string.""" + if isinstance(s_or_u, str): + return s_or_u.decode(encoding, errors=errors) + else: + return s_or_u + + def dict_merge(a, b): """ Recursively deep-merges two dictionaries. diff --git a/src/octoprint/util/pip.py b/src/octoprint/util/pip.py index 7d3d1da1..980e5735 100644 --- a/src/octoprint/util/pip.py +++ b/src/octoprint/util/pip.py @@ -11,6 +11,9 @@ import sys import logging +from octoprint.util import to_unicode + + class UnknownPip(Exception): pass @@ -77,11 +80,13 @@ class PipCaller(object): while p.returncode is None: line = p.stderr.readline(timeout=0.5) if line: + line = to_unicode(line, errors="replace") self._log_stderr(line) all_stderr.append(line) line = p.stdout.readline(timeout=0.5) if line: + line = to_unicode(line, errors="replace") self._log_stdout(line) all_stdout.append(line) From 885e6f916cf7d8a9085424d75b59af8afb7bb639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 2 Jul 2015 15:20:07 +0200 Subject: [PATCH 3/8] Fix: Don't persists checks when saving SWUpdate settings (cherry picked from commit 8d10be6) --- .../plugins/softwareupdate/__init__.py | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index 7cfdd2f3..157f671b 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -142,15 +142,29 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, "cache_ttl": 24 * 60, } + def on_settings_load(self): + data = dict(octoprint.plugin.SettingsPlugin.on_settings_load(self)) + if "checks" in data: + del data["checks"] + return data + def on_settings_save(self, data): - octoprint.plugin.SettingsPlugin.on_settings_save(self, data) + for key in self.get_settings_defaults(): + if key == "checks" or key == "cache_ttl": + 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 def get_settings_version(self): - return 2 + return 3 def on_settings_migrate(self, target, current=None): - if current is 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 @@ -159,14 +173,17 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if configured_checks is None: configured_checks = dict() + check_keys = configured_checks.keys() + # take care of the octoprint entry if "octoprint" in configured_checks: octoprint_check = dict(configured_checks["octoprint"]) - if "type" in octoprint_check and not octoprint_check["type"] == "github_commit": - deletables=["current"] + if "type" not in octoprint_check or octoprint_check["type"] != "github_commit": + deletables=["current", "displayName", "displayVersion"] else: deletables=[] octoprint_check = self._clean_settings_check("octoprint", octoprint_check, self.get_settings_defaults()["checks"]["octoprint"], delete=deletables, save=False) + check_keys.remove("octoprint") # and the hooks update_check_hooks = self._plugin_manager.get_hooks("octoprint.plugin.softwareupdate.check_config") @@ -180,12 +197,20 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, 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": + if "type" not in merged or merged["type"] != "github_commit": deletables = ["current", "displayVersion"] else: deletables = [] self._clean_settings_check(key, settings_check, data, delete=deletables, save=False) + check_keys.remove(key) + + # and anything that's left over we'll just remove now + for key in check_keys: + dummy_defaults = dict(plugins=dict()) + dummy_defaults["plugins"][self._identifier] = dict(checks=dict()) + dummy_defaults["plugins"][self._identifier]["checks"][key] = None + self._settings.set(["checks", key], None, defaults=dummy_defaults) elif current == 1: configured_checks = self._settings.get(["checks"], incl_defaults=False) From 95c26a7850e9a80cad635ee4c4937cb169e5f189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 2 Jul 2015 15:21:05 +0200 Subject: [PATCH 4/8] Fix: Don't automatically persist or return _config_version Should only ever be read or written by the plugin system itself, not by on_settings_save or on_settings_load (cherry picked from commit 77f7d59) --- src/octoprint/plugin/types.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index ad74364c..bf702cb6 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -781,7 +781,10 @@ class SettingsPlugin(OctoPrintPlugin): :return: the current settings of the plugin, as a dictionary """ - return self._settings.get([], asdict=True, merged=True) + data = self._settings.get([], asdict=True, merged=True) + if "_config_version" in data: + del data["_config_version"] + return data def on_settings_save(self, data): """ @@ -803,9 +806,12 @@ class SettingsPlugin(OctoPrintPlugin): """ import octoprint.util + if "_config_version" in data: + del data["_config_version"] + current = self._settings.get([], asdict=True, merged=True) - data = octoprint.util.dict_merge(current, data) - self._settings.set([], data) + merged = octoprint.util.dict_merge(current, data) + self._settings.set([], merged) def get_settings_defaults(self): """ From 85ad85bdfa65d3d2bd5e57b67f37e1a9b6b4ed5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sat, 4 Jul 2015 23:51:29 +0200 Subject: [PATCH 5/8] Also refer to wiki from docs until everything "official" is migrated --- docs/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 06895909..1e619c43 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,6 +6,10 @@ Welcome to OctoPrint's documentation! :alt: The OctoPrint Logo :align: right +This documentation is still in the process of being migrated from +`OctoPrint's wiki `_, so also take +a look there! + Contents ======== From d854b41ffb3645d98df45fab399f9bbfd28d0157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 5 Jul 2015 00:09:13 +0200 Subject: [PATCH 6/8] Migrated access control docs from wiki --- docs/features/accesscontrol.rst | 69 +++++++++++++++++++++++++++++++ docs/features/custom_controls.rst | 16 ++++++- docs/features/index.rst | 1 + docs/features/plugins.rst | 2 +- 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 docs/features/accesscontrol.rst diff --git a/docs/features/accesscontrol.rst b/docs/features/accesscontrol.rst new file mode 100644 index 00000000..dd59bd6b --- /dev/null +++ b/docs/features/accesscontrol.rst @@ -0,0 +1,69 @@ +.. _sec-features-access_control: + +Access Control +============== + +When Access Control is enabled, anonymous users (not logged in) will only see +the read-only parts of the UI which are the following: + + * printer state + * available gcode files and stats (upload is disabled) + * temperature + * webcam + * gcode viewer + * terminal output (sending commands is disabled) + * available timelapse movies + * any components provided through plugins which are enabled for anonymous + users + +Logged in users will get access to everything besides the Settings and System +Commands, which are admin-only. + +If Access Control is disabled, everything is directly accessible. **That also +includes all administrative functionality as well as full control over the +printer!** + +Upon first start a configuration wizard is provided which allows configuration +of the first administrator account or alternatively disabling Access Control +(which is **NOT** recommended for systems that are directly accessible via the +Internet!). + +.. hint:: + + If you plan to have your OctoPrint instance accessible over the internet, + **always enable Access Control**. + +.. _sec-features-access_control-rerunning_wizard: + +Rerunning the wizard +-------------------- + +In case Access Control was disabled in the configuration wizard, it is +possibly to re-run it by editing ``config.yaml`` [#f1]_ and setting ``firstRun`` +in the ``server`` section and ``enabled`` in the ``accessControl`` section to +``true``: + +.. code-block-ext:: yaml + + accessControl: + enabled: true + # ... + server: + firstRun: true + +Then restart the server and connect to the web interface - the wizard should +be shown again. + +.. note:: + + If user accounts were created prior to disabling Access Control and those + user accounts are not to be used any more, remove ``.octoprint/users.yaml``. + If you don't remove this file, the above changes won't lead to the + configuration being shown again, instead Access Control will just be + enabled using the already existing login data. This is to prevent you from + resetting access control by accident. + +.. rubric:: Footnotes + +.. [#f1] For Linux that will be ``~/.octoprint/config.yaml``, for Windows it will be ``%APPDATA%/OctoPrint/config.yaml`` and for + Mac ``~/Library/Application Support/OctoPrint/config.yaml`` diff --git a/docs/features/custom_controls.rst b/docs/features/custom_controls.rst index a904748f..983611fc 100644 --- a/docs/features/custom_controls.rst +++ b/docs/features/custom_controls.rst @@ -10,7 +10,7 @@ buttons which trigger sending of one or more lines of GCODE to the printer over parameterization of these commands with values entered by the user to full blown GCODE script templates backed by `Jinja2 `_. -Custom controls are configured within :ref:`config.yaml ` in a ``controls`` section which +Custom controls are configured within :ref:`config.yaml ` [#f1]_ in a ``controls`` section which basically represents a hierarchical structure of all configured custom controls of various types. .. note:: @@ -94,6 +94,13 @@ button that sends one or more commands to the printer when clicked, displaying o controls that just serve as *container* for other controls, the latter being identified by having a ``children`` attribute wrapping more controls. +.. hint:: + + Take a look at the `Custom Control Editor plugin `_ + which allows you configuring your Custom Controls through OctoPrint's + settings interface without the need to manually edit the configuration + file. + .. _sec-features-custom_controls-types: Types @@ -278,4 +285,9 @@ Parameterized GCODE Script G28 X0 Y0 Note the usage of the ``parameters.repetitions`` template variable in the GCODE script template, which will contain -the value selected by the user for the "Go arounds" slider. \ No newline at end of file +the value selected by the user for the "Go arounds" slider. + +.. rubric:: Footnotes + +.. [#f1] For Linux that will be ``~/.octoprint/config.yaml``, for Windows it will be ``%APPDATA%/OctoPrint/config.yaml`` and for + Mac ``~/Library/Application Support/OctoPrint/config.yaml`` diff --git a/docs/features/index.rst b/docs/features/index.rst index 5ebc8620..c4b1a713 100644 --- a/docs/features/index.rst +++ b/docs/features/index.rst @@ -7,6 +7,7 @@ Features .. toctree:: :maxdepth: 2 + accesscontrol.rst custom_controls.rst gcode_scripts.rst action_commands.rst diff --git a/docs/features/plugins.rst b/docs/features/plugins.rst index c8fb1fd5..1b65b6dc 100644 --- a/docs/features/plugins.rst +++ b/docs/features/plugins.rst @@ -64,7 +64,7 @@ See :ref:`Developing Plugins `. .. rubric:: Footnotes .. [#f1] For Linux that will be ``~/.octoprint/plugins``, for Windows it will be ``%APPDATA%/OctoPrint/plugins`` and for - Mac ``~/Library/Application Support/OctoPrint`` + Mac ``~/Library/Application Support/OctoPrint/plugins`` .. [#f2] Make sure to use the exact same Python installation for installing the plugin that you also used for installing & running OctoPrint. For OctoPi this means using ``~/oprint/bin/pip`` for installing plugins instead of just ``pip``. From 19b4a0f403437917fa223a6f2de7efd84dcc2941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 5 Jul 2015 09:35:49 +0200 Subject: [PATCH 7/8] Fix: Always delete files from watched folder Wasn't ensure previously when using file preprocessors. --- src/octoprint/server/util/watchdog.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/octoprint/server/util/watchdog.py b/src/octoprint/server/util/watchdog.py index ae9e33c3..8bda659b 100644 --- a/src/octoprint/server/util/watchdog.py +++ b/src/octoprint/server/util/watchdog.py @@ -56,6 +56,11 @@ class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler): file_wrapper.filename, file_wrapper, allow_overwrite=True) + if os.path.exists(path): + try: + os.remove(path) + except: + self._logger.exception("Error while trying to clear a file from the watched folder") def on_created(self, event): self._upload(event.src_path) From 29d49179a9fc4666d5973438e2aabb6c7cb0234b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 5 Jul 2015 10:04:06 +0200 Subject: [PATCH 8/8] SWU Fix: properly sanitize version strings for comparison --- .../version_checks/github_release.py | 77 ++++++++++++++----- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/version_checks/github_release.py b/src/octoprint/plugins/softwareupdate/version_checks/github_release.py index 0d2abf69..5acddc16 100644 --- a/src/octoprint/plugins/softwareupdate/version_checks/github_release.py +++ b/src/octoprint/plugins/softwareupdate/version_checks/github_release.py @@ -44,35 +44,71 @@ def _get_latest_release(user, repo, include_prerelease=False): return latest["name"], latest["tag_name"] -def _is_current(release_information, compare_type, custom=None): +def _get_sanitized_version(version_string): + if "-" in version_string: + version_string = version_string[:version_string.find("-")] + return version_string + + +def _get_comparable_version_pkg_resources(version_string, force_base=True): + import pkg_resources + + version = pkg_resources.parse_version(version_string) + + if force_base: + if isinstance(version, tuple): + # old setuptools + base_version = [] + for part in version: + if part.startswith("*"): + break + base_version.append(part) + version = tuple(base_version) + else: + # new setuptools + version = pkg_resources.parse_version(version.base_version) + + return version + + +def _get_comparable_version_semantic(version_string, force_base=True): + import semantic_version + + version = semantic_version.Version.coerce(version_string, partial=False) + + if force_base: + version_string = "{}.{}.{}".format(version.major, version.minor, version.patch) + version = semantic_version.Version.coerce(version_string, partial=False) + + return version + + +def _is_current(release_information, compare_type, custom=None, force_base=True): if release_information["remote"]["value"] is None: return True if not compare_type in ("python", "semantic", "unequal", "custom") or compare_type == "custom" and custom is None: compare_type = "python" + sanitized_local = _get_sanitized_version(release_information["local"]["value"]) + sanitized_remote = _get_sanitized_version(release_information["remote"]["value"]) + try: if compare_type == "python": - import pkg_resources - - local_version = pkg_resources.parse_version(release_information["local"]["value"]) - remote_version = pkg_resources.parse_version(release_information["remote"]["value"]) - + local_version = _get_comparable_version_pkg_resources(sanitized_local, force_base=force_base) + remote_version = _get_comparable_version_pkg_resources(sanitized_remote, force_base=force_base) return local_version >= remote_version elif compare_type == "semantic": - import semantic_version - - local_version = semantic_version.Version(release_information["local"]["value"]) - remote_version = semantic_version.Version(release_information["remote"]["value"]) - + local_version = _get_comparable_version_semantic(sanitized_local, force_base=force_base) + remote_version = _get_comparable_version_semantic(sanitized_remote, force_base=force_base) return local_version >= remote_version elif compare_type == "custom": - return custom(release_information["local"], release_information["remote"]) + return custom(sanitized_local, sanitized_remote) else: - return release_information["local"]["value"] == release_information["remote"]["value"] + return sanitized_local == sanitized_remote except: logger.exception("Could not check if version is current due to an error, assuming it is") return True @@ -82,11 +118,13 @@ def get_latest(target, check, custom_compare=None): if not "user" in check or not "repo" in check: raise ConfigurationInvalid("github_release update configuration for %s needs user and repo set" % target) - current = None - if "current" in check: - current = check["current"] + current = check.get("current", None) + include_prerelease = check.get("prerelease", False) + force_base = check.get("force_base", True) - remote_name, remote_tag = _get_latest_release(check["user"], check["repo"], include_prerelease=check["prerelease"] == True if "prerelease" in check else False) + remote_name, remote_tag = _get_latest_release(check["user"], + check["repo"], + include_prerelease=include_prerelease) compare_type = check["release_compare"] if "release_compare" in check else "python" information =dict( @@ -96,4 +134,7 @@ def get_latest(target, check, custom_compare=None): logger.debug("Target: %s, local: %s, remote: %s" % (target, current, remote_tag)) - return information, _is_current(information, compare_type, custom=custom_compare) + return information, _is_current(information, + compare_type, + custom=custom_compare, + force_base=force_base)