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.