diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7d320fbb..eba13f2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -74,6 +74,20 @@
* [#1047](https://github.com/foosel/OctoPrint/issues/1047) - Fixed 90 degree
webcam rotation for iOS Safari.
+## 1.2.17rc2 (2016-10-13)
+
+### Improvements
+
+ * Improved the `serial.log` logging handler to roll over serial log on new connections to the printer instead of continuously appending to the same file. Please note that `serial.log` is a debugging tool only and should *not* be left enabled unless you are trying to troubleshoot something in your printer communication.
+ * Split JS/CSS/LESS asset bundles according into asset bundles for core + bundled plugins ("packed_core.{js|css|less}") and third party plugins ("packed_plugins.{js|css|less}"). That will allow the core UI to still function properly even if an installed third party plugin produces invalid JS and therefore causes a parser error for the whole plugin JS file. See [#1544](https://github.com/foosel/OctoPrint/issues/1544) for an example of such a situation.
+
+### Bug fixes
+
+ * Fixed a bug causing the update of OctoPrint to not work under certain circumstances: If 1.2.16 was installed and the settings were *never* saved via the "Settings" dialog's "Save", the update of OctoPrint would fail due to a `KeyError` in the updater. Reason is a renamed property, properly switched to when saving the settings.
+ * Fixed the logging subsystem to not properly clean up after itself.
+
+([Commits](https://github.com/foosel/OctoPrint/compare/1.2.17rc1...1.2.17rc2))
+
## 1.2.17rc1 (2016-10-06)
### Improvements
diff --git a/docs/plugins/injectedproperties.rst b/docs/plugins/injectedproperties.rst
index 308b9d9e..4fbf2a3b 100644
--- a/docs/plugins/injectedproperties.rst
+++ b/docs/plugins/injectedproperties.rst
@@ -12,6 +12,8 @@ An overview of these properties follows.
The plugin's name, as taken from either the ``__plugin_name__`` control property or the package info.
``self._plugin_version``
The plugin's version, as taken from either the ``__plugin_version__`` control property or the package info.
+``self._plugin_info``
+ The :class:`octoprint.plugin.core.PluginInfo` object associated with the plugin.
``self._basefolder``
The plugin's base folder where it's installed. Can be used to refer to files relative to the plugin's installation
location, e.g. included scripts, templates or assets.
diff --git a/src/octoprint/__init__.py b/src/octoprint/__init__.py
index 21f79381..389fd224 100644
--- a/src/octoprint/__init__.py
+++ b/src/octoprint/__init__.py
@@ -3,7 +3,7 @@
from __future__ import absolute_import, division, print_function
import sys
-import logging
+import logging as log
#~~ version
@@ -20,7 +20,7 @@ del get_versions
#~~ sane logging defaults
-logging.basicConfig()
+log.basicConfig()
#~~ try to ensure a sound SSL environment
@@ -108,6 +108,9 @@ def init_logging(settings, use_logging_file=True, logging_file=None, default_con
"formatters": {
"simple": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+ },
+ "serial": {
+ "format": "%(asctime)s - %(message)s"
}
},
"handlers": {
@@ -118,18 +121,18 @@ def init_logging(settings, use_logging_file=True, logging_file=None, default_con
"stream": "ext://sys.stdout"
},
"file": {
- "class": "logging.handlers.TimedRotatingFileHandler",
+ "class": "octoprint.logging.handlers.CleaningTimedRotatingFileHandler",
"level": "DEBUG",
"formatter": "simple",
"when": "D",
- "backupCount": "1",
+ "backupCount": 6,
"filename": os.path.join(settings.getBaseFolder("logs"), "octoprint.log")
},
"serialFile": {
- "class": "logging.handlers.RotatingFileHandler",
+ "class": "octoprint.logging.handlers.SerialLogHandler",
"level": "DEBUG",
- "formatter": "simple",
- "maxBytes": 2 * 1024 * 1024, # let's limit the serial log to 2MB in size
+ "formatter": "serial",
+ "backupCount": 3,
"filename": os.path.join(settings.getBaseFolder("logs"), "serial.log")
}
},
@@ -180,11 +183,11 @@ def init_logging(settings, use_logging_file=True, logging_file=None, default_con
config = default_config
# configure logging globally
- import logging.config
- logging.config.dictConfig(config)
+ import logging.config as logconfig
+ logconfig.dictConfig(config)
# make sure we log any warnings
- logging.captureWarnings(True)
+ log.captureWarnings(True)
import warnings
@@ -197,9 +200,9 @@ def init_logging(settings, use_logging_file=True, logging_file=None, default_con
# make sure we also log any uncaught exceptions
if uncaught_logger is None:
- logger = logging.getLogger(__name__)
+ logger = log.getLogger(__name__)
else:
- logger = logging.getLogger(uncaught_logger)
+ logger = log.getLogger(uncaught_logger)
if uncaught_handler is None:
def exception_logger(exc_type, exc_value, exc_tb):
@@ -216,7 +219,7 @@ def init_pluginsystem(settings):
from octoprint.plugin import plugin_manager
pm = plugin_manager(init=True, settings=settings)
- logger = logging.getLogger(__name__)
+ logger = log.getLogger(__name__)
settings_overlays = dict()
disabled_from_overlays = dict()
diff --git a/src/octoprint/logging/__init__.py b/src/octoprint/logging/__init__.py
new file mode 100644
index 00000000..6e016b16
--- /dev/null
+++ b/src/octoprint/logging/__init__.py
@@ -0,0 +1,4 @@
+# coding=utf-8
+from __future__ import absolute_import
+
+from . import handlers
diff --git a/src/octoprint/logging/handlers.py b/src/octoprint/logging/handlers.py
new file mode 100644
index 00000000..fb613429
--- /dev/null
+++ b/src/octoprint/logging/handlers.py
@@ -0,0 +1,83 @@
+# coding=utf-8
+from __future__ import absolute_import
+
+import logging.handlers
+import os
+import re
+import time
+
+class CleaningTimedRotatingFileHandler(logging.handlers.TimedRotatingFileHandler):
+
+ def __init__(self, *args, **kwargs):
+ logging.handlers.TimedRotatingFileHandler.__init__(self, *args, **kwargs)
+
+ # clean up old files on handler start
+ if self.backupCount > 0:
+ for s in self.getFilesToDelete():
+ os.remove(s)
+
+
+class SerialLogHandler(logging.handlers.RotatingFileHandler):
+
+ _do_rollover = False
+ _suffix_template = "%Y-%m-%d_%H-%M-%S"
+ _file_pattern = re.compile(r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$")
+
+ @classmethod
+ def on_open_connection(cls):
+ cls._do_rollover = True
+
+ def __init__(self, *args, **kwargs):
+ logging.handlers.RotatingFileHandler.__init__(self, *args, **kwargs)
+ self.cleanupFiles()
+
+ def emit(self, record):
+ logging.handlers.RotatingFileHandler.emit(self, record)
+
+ def shouldRollover(self, record):
+ return self.__class__._do_rollover
+
+ def getFilesToDelete(self):
+ """
+ Determine the files to delete when rolling over.
+ """
+ dirName, baseName = os.path.split(self.baseFilename)
+ fileNames = os.listdir(dirName)
+ result = []
+ prefix = baseName + "."
+ plen = len(prefix)
+ for fileName in fileNames:
+ if fileName[:plen] == prefix:
+ suffix = fileName[plen:]
+ if self.__class__._file_pattern.match(suffix):
+ result.append(os.path.join(dirName, fileName))
+ result.sort()
+ if len(result) < self.backupCount:
+ result = []
+ else:
+ result = result[:len(result) - self.backupCount]
+ return result
+
+ def cleanupFiles(self):
+ if self.backupCount > 0:
+ for path in self.getFilesToDelete():
+ os.remove(path)
+
+ def doRollover(self):
+ self.__class__._do_rollover = False
+
+ if self.stream:
+ self.stream.close()
+ self.stream = None
+
+ if os.path.exists(self.baseFilename):
+ # figure out creation date/time to use for file suffix
+ t = time.localtime(os.stat(self.baseFilename).st_mtime)
+ dfn = self.baseFilename + "." + time.strftime(self.__class__._suffix_template, t)
+ if os.path.exists(dfn):
+ os.remove(dfn)
+ os.rename(self.baseFilename, dfn)
+
+ self.cleanupFiles()
+ if not self.delay:
+ self.stream = self._open()
diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py
index d74b1f23..944700ee 100644
--- a/src/octoprint/plugin/core.py
+++ b/src/octoprint/plugin/core.py
@@ -1000,6 +1000,7 @@ class PluginManager(object):
identifier=name,
plugin_name=plugin.name,
plugin_version=plugin.version,
+ plugin_info=plugin,
basefolder=os.path.realpath(plugin.location),
logger=logging.getLogger(self.logging_prefix + name),
))
diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py
index 31ed4a79..cca716cc 100644
--- a/src/octoprint/plugins/cura/__init__.py
+++ b/src/octoprint/plugins/cura/__init__.py
@@ -70,7 +70,8 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
def on_startup(self, host, port):
# setup our custom logger
- cura_logging_handler = logging.handlers.RotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="engine"), maxBytes=2*1024*1024)
+ from octoprint.logging.handlers import CleaningTimedRotatingFileHandler
+ cura_logging_handler = CleaningTimedRotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="engine"), when="D", backupCount=3)
cura_logging_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
cura_logging_handler.setLevel(logging.DEBUG)
diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py
index 51f002a8..914d0a03 100644
--- a/src/octoprint/plugins/pluginmanager/__init__.py
+++ b/src/octoprint/plugins/pluginmanager/__init__.py
@@ -66,7 +66,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
##~~ StartupPlugin
def on_startup(self, host, port):
- console_logging_handler = logging.handlers.RotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="console"), maxBytes=2*1024*1024)
+ from octoprint.logging.handlers import CleaningTimedRotatingFileHandler
+ console_logging_handler = CleaningTimedRotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="console"), when="D", backupCount=3)
console_logging_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
console_logging_handler.setLevel(logging.DEBUG)
diff --git a/src/octoprint/plugins/softwareupdate/updaters/update_script.py b/src/octoprint/plugins/softwareupdate/updaters/update_script.py
index 009f622a..b47a7703 100644
--- a/src/octoprint/plugins/softwareupdate/updaters/update_script.py
+++ b/src/octoprint/plugins/softwareupdate/updaters/update_script.py
@@ -59,7 +59,7 @@ def perform_update(target, check, target_version, log_cb=None):
update_script = check["update_script"]
update_branch = check.get("update_branch", "")
force_exact_version = check.get("force_exact_version", False)
- folder = check.get("update_folder", check["checkout_folder"])
+ folder = check.get("update_folder", check.get("checkout_folder")) # either should be set, tested above
pre_update_script = check.get("pre_update_script", None)
post_update_script = check.get("post_update_script", None)
diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py
index af51d7e9..14bd0960 100644
--- a/src/octoprint/printer/standard.py
+++ b/src/octoprint/printer/standard.py
@@ -197,6 +197,10 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
eventManager().fire(Events.CONNECTING)
self._printerProfileManager.select(profile)
+
+ from octoprint.logging.handlers import SerialLogHandler
+ SerialLogHandler.on_open_connection()
+
self._comm = comm.MachineCom(port, baudrate, callbackObject=self, printerProfileManager=self._printerProfileManager)
def disconnect(self):
diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py
index 7e98e146..bbf89430 100644
--- a/src/octoprint/server/__init__.py
+++ b/src/octoprint/server/__init__.py
@@ -177,7 +177,6 @@ class Server(object):
if self._settings.getBoolean(["serial", "log"]):
# enable debug logging to serial.log
logging.getLogger("SERIAL").setLevel(logging.DEBUG)
- logging.getLogger("SERIAL").debug("Enabling serial logging")
# start the intermediary server
self._start_intermediary_server()
@@ -981,7 +980,8 @@ class Server(object):
preferred_stylesheet = self._settings.get(["devel", "stylesheet"])
minify = self._settings.getBoolean(["devel", "webassets", "minify"])
- dynamic_assets = util.flask.collect_plugin_assets(
+ dynamic_core_assets = util.flask.collect_core_assets(enable_gcodeviewer=enable_gcodeviewer)
+ dynamic_plugin_assets = util.flask.collect_plugin_assets(
enable_gcodeviewer=enable_gcodeviewer,
preferred_stylesheet=preferred_stylesheet
)
@@ -1038,11 +1038,13 @@ class Server(object):
"js/app/client/util.js",
"js/app/client/wizard.js"
]
- js_app = dynamic_assets["js"] + [
- "js/app/dataupdater.js",
- "js/app/helpers.js",
- "js/app/main.js",
- ]
+ js_core = dynamic_core_assets["js"] + \
+ dynamic_plugin_assets["bundled"]["js"] + \
+ ["js/app/dataupdater.js",
+ "js/app/helpers.js",
+ "js/app/main.js"]
+ js_plugins = dynamic_plugin_assets["external"]["js"]
+ js_app = js_core + js_plugins
css_libs = [
"css/bootstrap.min.css",
@@ -1053,8 +1055,13 @@ class Server(object):
"css/jquery.fileupload-ui.css",
"css/pnotify.min.css"
]
- css_app = list(dynamic_assets["css"])
- less_app = list(dynamic_assets["less"])
+ css_core = list(dynamic_core_assets["css"]) + list(dynamic_plugin_assets["bundled"]["css"])
+ css_plugins = list(dynamic_plugin_assets["external"]["css"])
+ css_app = css_core + css_plugins
+
+ less_core = list(dynamic_core_assets["less"]) + list(dynamic_plugin_assets["bundled"]["less"])
+ less_plugins = list(dynamic_plugin_assets["external"]["less"])
+ less_app = less_core + less_plugins
from webassets.filter import register_filter, Filter
from webassets.filter.cssrewrite.base import PatternRewriter
@@ -1075,7 +1082,7 @@ class Server(object):
return "{import_with_options}\"{import_url}\";".format(**locals())
- class JsDelimiterBundle(Filter):
+ class JsDelimiterBundler(Filter):
name = "js_delimiter_bundler"
options = {}
def input(self, _in, out, **kwargs):
@@ -1083,34 +1090,68 @@ class Server(object):
out.write("\n;\n")
register_filter(LessImportRewrite)
- register_filter(JsDelimiterBundle)
+ register_filter(JsDelimiterBundler)
+ # JS
js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js", filters="js_delimiter_bundler")
if minify:
js_client_bundle = Bundle(*js_client, output="webassets/packed_client.js", filters="rjsmin, js_delimiter_bundler")
+ js_core_bundle = Bundle(*js_core, output="webassets/packed_core.js", filters="rjsmin, js_delimiter_bundler")
+ js_plugins_bundle = Bundle(*js_plugins, output="webassets/packed_plugins.js", filters="rjsmin, js_delimiter_bundler")
js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="rjsmin, js_delimiter_bundler")
else:
js_client_bundle = Bundle(*js_client, output="webassets/packed_client.js", filters="js_delimiter_bundler")
+ js_core_bundle = Bundle(*js_core, output="webassets/packed_core.js", filters="js_delimiter_bundler")
+ js_plugins_bundle = Bundle(*js_plugins, output="webassets/packed_plugins.js", filters="js_delimiter_bundler")
js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="js_delimiter_bundler")
+ # CSS
css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css")
+ if len(css_core) == 0:
+ css_core_bundle = Bundle(*[])
+ else:
+ css_core_bundle = Bundle(*css_app, output="webassets/packed_core.css", filters="cssrewrite")
+
+ if len(css_plugins) == 0:
+ css_plugins_bundle = Bundle(*[])
+ else:
+ css_plugins_bundle = Bundle(*css_app, output="webassets/packed_plugins.css", filters="cssrewrite")
+
if len(css_app) == 0:
css_app_bundle = Bundle(*[])
else:
css_app_bundle = Bundle(*css_app, output="webassets/packed_app.css", filters="cssrewrite")
- if len(less_app) == 0:
- all_less_bundle = Bundle(*[])
+ # LESS
+ if len(less_core) == 0:
+ less_core_bundle = Bundle(*[])
else:
- all_less_bundle = Bundle(*less_app, output="webassets/packed_app.less", filters="cssrewrite, less_importrewrite")
+ less_core_bundle = Bundle(*less_app, output="webassets/packed_core.less", filters="cssrewrite, less_importrewrite")
+ if len(less_plugins) == 0:
+ less_plugins_bundle = Bundle(*[])
+ else:
+ less_plugins_bundle = Bundle(*less_app, output="webassets/packed_plugins.less", filters="cssrewrite, less_importrewrite")
+
+ if len(less_app) == 0:
+ less_app_bundle = Bundle(*[])
+ else:
+ less_app_bundle = Bundle(*less_app, output="webassets/packed_app.less", filters="cssrewrite, less_importrewrite")
+
+ # asset registration
assets.register("js_libs", js_libs_bundle)
assets.register("js_client", js_client_bundle)
+ assets.register("js_core", js_core_bundle)
+ assets.register("js_plugins", js_plugins_bundle)
assets.register("js_app", js_app_bundle)
assets.register("css_libs", css_libs_bundle)
+ assets.register("css_core", css_core_bundle)
+ assets.register("css_plugins", css_plugins_bundle)
assets.register("css_app", css_app_bundle)
- assets.register("less_app", all_less_bundle)
+ assets.register("less_core", less_core_bundle)
+ assets.register("less_plugins", less_plugins_bundle)
+ assets.register("less_app", less_app_bundle)
def _start_intermediary_server(self):
import BaseHTTPServer
diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py
index f4281ee1..6ef34ae1 100644
--- a/src/octoprint/server/util/flask.py
+++ b/src/octoprint/server/util/flask.py
@@ -1234,12 +1234,8 @@ class SettingsCheckUpdater(webassets.updater.BaseUpdater):
cache_value = webassets.utils.hash_func(json.dumps(settings().effective_yaml))
ctx.cache.set(cache_key, cache_value)
-##~~ plugin assets collector
-
-def collect_plugin_assets(enable_gcodeviewer=True, preferred_stylesheet="css"):
- logger = logging.getLogger(__name__ + ".collect_plugin_assets")
-
- supported_stylesheets = ("css", "less")
+##~~ core assets collector
+def collect_core_assets(enable_gcodeviewer=True, preferred_stylesheet="css"):
assets = dict(
js=[],
css=[],
@@ -1288,9 +1284,24 @@ def collect_plugin_assets(enable_gcodeviewer=True, preferred_stylesheet="css"):
elif preferred_stylesheet == "css":
assets["css"].append('css/octoprint.css')
+ return assets
+
+##~~ plugin assets collector
+
+def collect_plugin_assets(enable_gcodeviewer=True, preferred_stylesheet="css"):
+ logger = logging.getLogger(__name__ + ".collect_plugin_assets")
+
+ supported_stylesheets = ("css", "less")
+ assets = dict(bundled=dict(js=[], css=[], less=[]),
+ external=dict(js=[], css=[], less=[]))
+
asset_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.AssetPlugin)
for implementation in asset_plugins:
name = implementation._identifier
+ is_bundled = implementation._plugin_info.bundled
+
+ asset_key = "bundled" if is_bundled else "external"
+
try:
all_assets = implementation.get_assets()
basefolder = implementation.get_asset_folder()
@@ -1308,13 +1319,13 @@ def collect_plugin_assets(enable_gcodeviewer=True, preferred_stylesheet="css"):
for asset in all_assets["js"]:
if not asset_exists("js", asset):
continue
- assets["js"].append('plugin/{name}/{asset}'.format(**locals()))
+ assets[asset_key]["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()))
+ assets[asset_key][preferred_stylesheet].append('plugin/{name}/{asset}'.format(**locals()))
else:
for stylesheet in supported_stylesheets:
if not stylesheet in all_assets:
@@ -1323,7 +1334,7 @@ def collect_plugin_assets(enable_gcodeviewer=True, preferred_stylesheet="css"):
for asset in all_assets[stylesheet]:
if not asset_exists(stylesheet, asset):
continue
- assets[stylesheet].append('plugin/{name}/{asset}'.format(**locals()))
+ assets[asset_key][stylesheet].append('plugin/{name}/{asset}'.format(**locals()))
break
return assets
diff --git a/src/octoprint/templates/javascripts.jinja2 b/src/octoprint/templates/javascripts.jinja2
index f69b158c..7db1bfbb 100644
--- a/src/octoprint/templates/javascripts.jinja2
+++ b/src/octoprint/templates/javascripts.jinja2
@@ -1,15 +1,5 @@
-{% assets "js_libs" %}
-
-{% endassets %}
-
-{% assets "js_client" %}
-
-{% endassets %}
-
-{% assets "js_app" %}
-
-{% endassets %}
-
-{% if g.locale %}
-
-{% endif %}
+{% assets "js_libs" %}{% endassets %}
+{% assets "js_client" %}{% endassets %}
+{% assets "js_core" %}{% endassets %}
+{% assets "js_plugins" %}{% endassets %}
+{% if g.locale %}{% endif %}
diff --git a/src/octoprint/templates/stylesheets.jinja2 b/src/octoprint/templates/stylesheets.jinja2
index b88de6a0..95e7ae7a 100644
--- a/src/octoprint/templates/stylesheets.jinja2
+++ b/src/octoprint/templates/stylesheets.jinja2
@@ -1,13 +1,6 @@
-{% assets "css_libs" %}
-
-{% endassets %}
-
-{% assets "css_app" %}
-
-{% endassets %}
-
-{% assets "less_app" %}
-
-{% endassets %}
-
+{% assets "css_libs" %}{% endassets %}
+{% assets "css_core" %}{% endassets %}
+{% assets "css_plugins" %}{% endassets %}
+{% assets "less_core" %}{% endassets %}
+{% assets "less_plugins" %}{% endassets %}
diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py
index 398ac97a..3252b25b 100644
--- a/src/octoprint/util/__init__.py
+++ b/src/octoprint/util/__init__.py
@@ -1,4 +1,6 @@
# coding=utf-8
+from __future__ import absolute_import
+
"""
This module bundles commonly used utility methods or helper classes that are used in multiple places withing
OctoPrint's source code.