Merge branch 'maintenance' into devel

# Conflicts:
#	CHANGELOG.md
#	docs/plugins/concepts.rst
#	src/octoprint/__init__.py
#	src/octoprint/server/__init__.py
#	src/octoprint/templates/javascripts.jinja2
#	src/octoprint/timelapse.py

Merge branch 'devel' into dev/wizard

# Conflicts:
#	octoprint_octobullet/__init__.py
#	octoprint_octobullet/templates/octobullet_settings.jinja2

Merge branch 'devel' into dev/wizard

# Conflicts:
#	octoprint_octobullet/__init__.py
#	octoprint_octobullet/templates/octobullet_settings.jinja2
This commit is contained in:
Gina Häußge 2016-10-13 13:51:44 +02:00
commit 9de78001dd
15 changed files with 217 additions and 67 deletions

View file

@ -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

View file

@ -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.

View file

@ -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()

View file

@ -0,0 +1,4 @@
# coding=utf-8
from __future__ import absolute_import
from . import handlers

View file

@ -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()

View file

@ -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),
))

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -1,15 +1,5 @@
{% assets "js_libs" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% assets "js_client" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% assets "js_app" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% if g.locale %}
<script type="text/javascript" src="{{ url_for('localeJs', locale=g.locale, domain='messages') }}"></script>
{% endif %}
{% assets "js_libs" %}<script type="text/javascript" src="{{ ASSET_URL }}"></script>{% endassets %}
{% assets "js_client" %}<script type="text/javascript" src="{{ ASSET_URL }}"></script>{% endassets %}
{% assets "js_core" %}<script type="text/javascript" src="{{ ASSET_URL }}"></script>{% endassets %}
{% assets "js_plugins" %}<script type="text/javascript" src="{{ ASSET_URL }}"></script>{% endassets %}
{% if g.locale %}<script type="text/javascript" src="{{ url_for('localeJs', locale=g.locale, domain='messages') }}"></script>{% endif %}

View file

@ -1,13 +1,6 @@
{% assets "css_libs" %}
<link href="{{ ASSET_URL }}" rel="stylesheet" media="screen">
{% endassets %}
{% assets "css_app" %}
<link href="{{ ASSET_URL }}" rel="stylesheet" media="screen">
{% endassets %}
{% assets "less_app" %}
<link href="{{ ASSET_URL }}" rel="stylesheet/less" type="text/css" media="screen">
{% endassets %}
{% assets "css_libs" %}<link href="{{ ASSET_URL }}" rel="stylesheet" media="screen">{% endassets %}
{% assets "css_core" %}<link href="{{ ASSET_URL }}" rel="stylesheet" media="screen">{% endassets %}
{% assets "css_plugins" %}<link href="{{ ASSET_URL }}" rel="stylesheet" media="screen">{% endassets %}
{% assets "less_core" %}<link href="{{ ASSET_URL }}" rel="stylesheet/less" type="text/css" media="screen">{% endassets %}
{% assets "less_plugins" %}<link href="{{ ASSET_URL }}" rel="stylesheet/less" type="text/css" media="screen">{% endassets %}
<script src="{{ url_for('static', filename='js/lib/less.min.js') }}" type="text/javascript"></script>

View file

@ -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.