From 97826b2f3b7edf4196395fac310d6061cfa3725d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 8 Jul 2015 16:26:08 +0200 Subject: [PATCH 1/9] Fix: More resilience against missing plugin assets Two changes: * Asset existence will now be checked before they get included in the assets to bundle by webassets, logging a warning if a file isn't present. * Monkey-patched webassets filter chain to not die when a file doesn't exist, but to log an error instead and just return an empty file instead. (cherry picked from commit 2a5ec33) --- src/octoprint/server/__init__.py | 4 +++ src/octoprint/server/util/flask.py | 44 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 7839a9bd..b99358f4 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -544,6 +544,9 @@ class Server(): }, "tornado.general": { "level": "INFO" + }, + "octoprint.server.util.flask": { + "level": "WARN" } }, "root": { @@ -712,6 +715,7 @@ class Server(): global pluginManager util.flask.fix_webassets_cache() + util.flask.fix_webassets_filtertool() base_folder = settings().getBaseFolder("generated") diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index 0f3d4a05..75f03d24 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -19,6 +19,7 @@ import uuid import threading import logging import netaddr +import os from octoprint.settings import settings import octoprint.server @@ -174,6 +175,34 @@ def fix_webassets_cache(): cache.FilesystemCache.set = fixed_set cache.FilesystemCache.get = fixed_get +def fix_webassets_filtertool(): + from webassets.merge import FilterTool, log, MemoryHunk + + error_logger = logging.getLogger(__name__ + ".fix_webassets_filtertool") + + def fixed_wrap_cache(self, key, func): + """Return cache value ``key``, or run ``func``. + """ + if self.cache: + if not self.no_cache_read: + log.debug('Checking cache for key %s', key) + content = self.cache.get(key) + if not content in (False, None): + log.debug('Using cached result for %s', key) + return MemoryHunk(content) + + try: + content = func().getvalue() + if self.cache: + log.debug('Storing result in cache with key %s', key,) + self.cache.set(key, content) + return MemoryHunk(content) + except: + error_logger.exception("Got an exception while trying to apply filter, ignoring file") + return MemoryHunk("") + + FilterTool._wrap_cache = fixed_wrap_cache + #~~ passive login helper def passive_login(): @@ -535,6 +564,8 @@ class SettingsCheckUpdater(webassets.updater.BaseUpdater): ##~~ plugin assets collector def collect_plugin_assets(enable_gcodeviewer=True, enable_timelapse=True, preferred_stylesheet="css"): + logger = logging.getLogger(__name__ + ".collect_plugin_assets") + supported_stylesheets = ("css", "less") assets = dict( js=[], @@ -578,13 +609,24 @@ def collect_plugin_assets(enable_gcodeviewer=True, enable_timelapse=True, prefer for implementation in asset_plugins: name = implementation._identifier all_assets = implementation.get_assets() + basefolder = implementation.get_asset_folder() + + def asset_exists(category, asset): + exists = os.path.exists(os.path.join(basefolder, asset)) + if not exists: + logger.warn("Plugin {} is referring to non existing {} asset {}".format(name, category, asset)) + return exists if "js" in all_assets: for asset in all_assets["js"]: + if not asset_exists("js", asset): + continue assets["js"].append('plugin/{name}/{asset}'.format(**locals())) if preferred_stylesheet in all_assets: for asset in all_assets[preferred_stylesheet]: + if not asset_exists(preferred_stylesheet, asset): + continue assets[preferred_stylesheet].append('plugin/{name}/{asset}'.format(**locals())) else: for stylesheet in supported_stylesheets: @@ -592,6 +634,8 @@ def collect_plugin_assets(enable_gcodeviewer=True, enable_timelapse=True, prefer continue for asset in all_assets[stylesheet]: + if not asset_exists(stylesheet, asset): + continue assets[stylesheet].append('plugin/{name}/{asset}'.format(**locals())) break From 8d14ea6093a7fab65f840abbe66eb2b9cf239218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 8 Jul 2015 16:34:42 +0200 Subject: [PATCH 2/9] Rewrite urls in packed css and less files See also #962 (cherry picked from commit 7ea2ee2) --- src/octoprint/server/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index b99358f4..5430514a 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -849,9 +849,9 @@ class Server(): js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="js_delimiter_bundler") css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css") - css_app_bundle = Bundle(*css_app, output="webassets/packed_app.css") + css_app_bundle = Bundle(*css_app, output="webassets/packed_app.css", filters="cssrewrite") - all_less_bundle = Bundle(*less_app, output="webassets/packed_app.less", filters="less_importrewrite") + all_less_bundle = Bundle(*less_app, output="webassets/packed_app.less", filters="cssrewrite, less_importrewrite") assets.register("js_libs", js_libs_bundle) assets.register("js_app", js_app_bundle) From abbf937e7f2ef0a6c9eef7e729c7069f3f56aec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 8 Jul 2015 17:22:18 +0200 Subject: [PATCH 3/9] Preparing release of 1.2.3 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0c30fd6..31c9fac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # OctoPrint Changelog +## 1.2.3 (2015-07-09) + +### Improvements + + * New option to actively poll the watched folder. This should make it work also + if it is mounted on a filesystem that doesn't allow getting notifications + about added files through notification by the operating system (e.g. + network shares). + * Better resilience against senseless temperature/SD-status-polling intervals + (such as 0). + * Log exceptions during writing to the serial port to `octoprint.log`. + +### Bug Fixes + + * [#961](https://github.com/foosel/OctoPrint/pull/961) - Fixed a JavaScript error that caused an error to be logged when "enter" was pressed in file or plugin search. + * [#962](https://github.com/foosel/OctoPrint/pull/962) - ``url(...)``s in packed CSS and LESS files should now be rewritten properly too to refer to correct paths + * Update notifications were not vanishing properly after updating: + * Only use version cache for update notifications if the OctoPrint version still is the same to make sure the cache gets invalidated after an external update of OctoPrint. + * Do not persist version information when saving settings of the Software Update plugin + * Always delete files from the ``watched`` folder after importing then. Using file preprocessor plugins could lead to the files staying there. + * Fixed an encoding problem causing OctoPrint's Plugin Manager and Software Update plugins to choke on UTF-8 characters in the update output. + * Fixed sorting by file size in file list + * More resilience against missing plugin assets: + * Asset existence will now be checked before they get included + in the assets to bundle by webassets, logging a warning if a + file isn't present. + * Monkey-patched webassets filter chain to not die when a file + doesn't exist, but to log an error instead and just return + an empty file instead. + +([Commits](https://github.com/foosel/OctoPrint/compare/1.2.2...1.2.3)) + ## 1.2.2 (2015-06-30) ### Bug Fixes From 677e583345a0b1abde3eccb5b23ff10b487f1fef Mon Sep 17 00:00:00 2001 From: Mark Walker Date: Tue, 7 Jul 2015 23:24:27 +0200 Subject: [PATCH 4/9] pluginmanager: case handling and submit binding - Convert query term to lower case so that it is case insensitive both ways - Knockout submit binding takes the form element as parameter and return value determines whether the form submit occurs. See http://knockoutjs.com/documentation/submit-binding.html (cherry picked from commit 4a2cc53) --- .../plugins/pluginmanager/static/js/pluginmanager.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 8d0fb417..49fb4fee 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -147,19 +147,17 @@ $(function() { } }); - self.performRepositorySearch = function(e) { - if (e !== undefined) { - e.preventDefault(); - } - + self.performRepositorySearch = function(formElement) { var query = self.repositorySearchQuery(); if (query !== undefined && query.trim() != "") { + query = query.toLocaleLowerCase() self.repositoryplugins.changeSearchFunction(function(entry) { return entry && (entry["title"].toLocaleLowerCase().indexOf(query) > -1 || entry["description"].toLocaleLowerCase().indexOf(query) > -1); }); } else { self.repositoryplugins.resetSearch(); } + return false; }; self.fromResponse = function(data) { From e62cef590be9c3a8ae40a6db185a1e0aab717536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 8 Jul 2015 08:45:10 +0200 Subject: [PATCH 5/9] Fixed preventDefault issues also for files search (cherry picked from commit 4894c6e) --- src/octoprint/static/js/app/viewmodels/files.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index f87f9879..60e700cc 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -309,18 +309,17 @@ $(function() { }; self.performSearch = function(e) { - if (e !== undefined) { - e.preventDefault(); - } - var query = self.searchQuery(); if (query !== undefined && query.trim() != "") { + query = query.toLocaleLowerCase(); self.listHelper.changeSearchFunction(function(entry) { return entry && entry["name"].toLocaleLowerCase().indexOf(query) > -1; }); } else { self.listHelper.resetSearch(); } + + return false; }; self.onDataUpdaterReconnect = function() { From 3761995aff94acf32495556730f133a1626245b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 8 Jul 2015 08:45:36 +0200 Subject: [PATCH 6/9] Removed unneeded parameter and added missing semicolon (cherry picked from commit 29b3b62) --- .../plugins/pluginmanager/static/js/pluginmanager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 49fb4fee..eba85fad 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -147,10 +147,10 @@ $(function() { } }); - self.performRepositorySearch = function(formElement) { + self.performRepositorySearch = function() { var query = self.repositorySearchQuery(); if (query !== undefined && query.trim() != "") { - query = query.toLocaleLowerCase() + query = query.toLocaleLowerCase(); self.repositoryplugins.changeSearchFunction(function(entry) { return entry && (entry["title"].toLocaleLowerCase().indexOf(query) > -1 || entry["description"].toLocaleLowerCase().indexOf(query) > -1); }); From 5dfffc36e3f205b15cd41e46ac31c8bcfab4680c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 9 Jul 2015 09:44:51 +0200 Subject: [PATCH 7/9] maintenance branch is now 1.2.4-dev --- .versioneer-lookup | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.versioneer-lookup b/.versioneer-lookup index ea7606ff..7c9750a0 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.3 +# maintenance is currently the branch for preparation of maintenance release 1.2.4 # so are any fix/... branches -maintenance 1.2.3-dev 1c6b0554c796f03ed539397daa4b13c44d05a99d -fix/.* 1.2.3-dev 1c6b0554c796f03ed539397daa4b13c44d05a99d +maintenance 1.2.4-dev 3761995aff94acf32495556730f133a1626245b3 +fix/.* 1.2.4-dev 3761995aff94acf32495556730f133a1626245b3 # every other branch is a development branch and thus gets resolved to 1.3.0-dev for now .* 1.3.0-dev 198d3450d94be1a2 From f89cbc9133f65c691ec9abb96b0cc5216cb89cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 9 Jul 2015 09:27:38 +0200 Subject: [PATCH 8/9] Allow to not cache responses that set no-cache headers (cherry picked from commit 163100b) --- src/octoprint/server/util/flask.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index 75f03d24..b52264a2 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -244,7 +244,7 @@ def passive_login(): _cache = SimpleCache() -def cached(timeout=5 * 60, key=lambda: "view/%s" % flask.request.path, unless=None, refreshif=None): +def cached(timeout=5 * 60, key=lambda: "view/%s" % flask.request.path, unless=None, refreshif=None, unless_response=None): def decorator(f): @functools.wraps(f) def decorated_function(*args, **kwargs): @@ -273,6 +273,11 @@ def cached(timeout=5 * 60, key=lambda: "view/%s" % flask.request.path, unless=No logger.debug("No cache entry or refreshing cache for {path}, calling wrapped function".format(path=flask.request.path)) rv = f(*args, **kwargs) + # do not store if the "unless_response" condition is true + if callable(unless_response) and unless_response(rv): + logger.debug("Not caching result for {path}, bypassed".format(path=flask.request.path)) + return rv + # store it in the cache _cache.set(cache_key, rv, timeout=timeout) @@ -285,6 +290,23 @@ def cached(timeout=5 * 60, key=lambda: "view/%s" % flask.request.path, unless=No def cache_check_headers(): return "no-cache" in flask.request.cache_control or "no-cache" in flask.request.pragma +def cache_check_response_headers(response): + if not isinstance(response, flask.Response): + return False + + headers = response.headers + + if "Cache-Control" in headers and "no-cache" in headers["Cache-Control"]: + return True + + if "Pragma" in headers and "no-cache" in headers["Pragma"]: + return True + + if "Expires" in headers and headers["Expires"] in ("0", "-1"): + return True + + return False + #~~ access validators for use with tornado From 5470aaaa3a66261f031f6fae0523b274ce640423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 9 Jul 2015 09:28:31 +0200 Subject: [PATCH 9/9] Set no-cache headers on page while firstRun is True (cherry picked from commit f1afb70) --- src/octoprint/server/views.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index 318534f6..0e60d32a 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -21,7 +21,9 @@ import logging _logger = logging.getLogger(__name__) @app.route("/") -@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, key=lambda: "view/%s/%s" % (request.path, g.locale)) +@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, + key=lambda: "view/%s/%s" % (request.path, g.locale), + unless_response=util.flask.cache_check_response_headers) def index(): #~~ a bunch of settings @@ -240,12 +242,13 @@ def index(): #~~ prepare full set of template vars for rendering + first_run = settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()) render_kwargs = dict( webcamStream=settings().get(["webcam", "stream"]), enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]), enableAccessControl=userManager is not None, enableSdSupport=settings().get(["feature", "sdSupport"]), - firstRun=settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()), + firstRun=first_run, debug=debug, version=VERSION, display_version=DISPLAY_VERSION, @@ -260,10 +263,20 @@ def index(): #~~ render! - return render_template( + import datetime + + response = make_response(render_template( "index.jinja2", **render_kwargs - ) + )) + response.headers["Last-Modified"] = datetime.datetime.now() + + if first_run: + response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "-1" + + return response def _process_template_configs(name, implementation, configs, rules):