Merge branch 'devel' into dev/settingsOverlays

This commit is contained in:
Gina Häußge 2016-06-24 14:31:41 +02:00
commit 3893b700f6
6 changed files with 139 additions and 24 deletions

View file

@ -493,9 +493,9 @@ class PluginManager(object):
cmd.finalize_options()
self._python_install_dir = cmd.install_lib
self._python_prefix = sys.prefix
self._python_prefix = os.path.realpath(sys.prefix)
self._python_virtual_env = hasattr(sys, "real_prefix") \
or (hasattr(sys, "base_prefix") and sys.prefix != sys.base_prefix)
or (hasattr(sys, "base_prefix") and os.path.realpath(sys.prefix) != os.path.realpath(sys.base_prefix))
@property
def plugins(self):
@ -616,7 +616,11 @@ class PluginManager(object):
# of the virtual env, so this check is necessary
plugin.managable = os.access(plugin.location, os.W_OK) \
and (not self._python_virtual_env
or plugin.location.startswith(self._python_prefix))
or is_sub_path_of(plugin.location, self._python_prefix)
or is_editable_install(self._python_install_dir,
package_name,
module_name,
plugin.location))
plugin.enabled = False
result[key] = plugin
@ -1258,6 +1262,43 @@ class PluginManager(object):
raise ValueError("Invalid hook definition, neither a callable nor a 2-tuple (callback, order): {!r}".format(hook))
def is_sub_path_of(path, parent):
"""
Tests if `path` is a sub path (or identical) to `path`.
>>> is_sub_path_of("/a/b/c", "/a/b")
True
>>> is_sub_path_of("/a/b/c", "/a/b2")
False
>>> is_sub_path_of("/a/b/c", "/b/c")
False
>>> is_sub_path_of("/foo/bar/../../a/b/c", "/a/b")
True
>>> is_sub_path_of("/a/b", "/a/b")
True
"""
rel_path = os.path.relpath(os.path.realpath(path),
os.path.realpath(parent))
return not (rel_path == os.pardir or
rel_path.startswith(os.pardir + os.sep))
def is_editable_install(install_dir, package, module, location):
package_link = os.path.join(install_dir, "{}.egg-link".format(package))
if os.path.isfile(package_link):
expected_target = os.path.normcase(os.path.realpath(location))
try:
with open(package_link) as f:
contents = f.readlines()
for line in contents:
target = os.path.normcase(os.path.realpath(os.path.join(line.strip(), module)))
if target == expected_target:
return True
except:
pass
return False
class InstalledEntryPoint(pkginfo.Installed):
def __init__(self, entry_point, metadata_version=None):

View file

@ -741,6 +741,16 @@ class UiPlugin(OctoPrintPlugin, SortablePlugin):
"""
return None
def get_ui_preemptive_caching_enabled(self):
"""
Allows to control whether the view provided by the plugin should be preemptively
cached on server startup (default) or not.
Returns:
bool: Whether to enable preemptive caching or not
"""
return True
def get_ui_data_for_preemptive_caching(self):
"""
Allows defining additional data to be persisted in the preemptive cache configuration, on

View file

@ -752,6 +752,20 @@ class Server(object):
entries = reversed(sorted(cache_data[route], key=lambda x: x.get("_count", 0)))
for kwargs in entries:
plugin = kwargs.get("plugin", None)
if plugin:
try:
plugin_info = pluginManager.get_plugin_info(plugin, require_enabled=True)
implementation = plugin_info.implementation
if implementation is None or not isinstance(implementation, octoprint.plugin.UiPlugin):
self._logger.debug("Plugin {} is not a UiPlugin, preemptive caching makes no sense".format(plugin))
continue
if not implementation.get_ui_preemptive_caching_enabled():
self._logger.debug("Plugin {} has disabled preemptive caching".format(plugin))
continue
except:
self._logger.exception("Error while trying to check if plugin {} has preemptive caching enabled, skipping entry")
continue
additional_request_data = kwargs.get("_additional_request_data", dict())
kwargs = dict((k, v) for k, v in kwargs.items() if not k.startswith("_") and not k == "plugin")
kwargs.update(additional_request_data)
@ -761,8 +775,9 @@ class Server(object):
else:
self._logger.info("Preemptively caching {} for {!r}".format(route, kwargs))
builder = EnvironBuilder(**kwargs)
with preemptive_cache.disable_access_logging():
app(builder.get_environ(), lambda *a, **kw: None)
with preemptive_cache.cache_environment(dict(plugin=plugin if plugin is not None else "_default")):
with preemptive_cache.disable_access_logging():
app(builder.get_environ(), lambda *a, **kw: None)
except:
self._logger.exception("Error while trying to preemptively cache {} for {!r}".format(route, kwargs))

View file

@ -383,11 +383,14 @@ class PreemptiveCache(object):
def __init__(self, cachefile):
self.cachefile = cachefile
self.environment = None
self._lock = threading.RLock()
self._logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
self._log_access = True
self._lock = threading.RLock()
self._environment_lock = threading.RLock()
def record(self, data, unless=None):
if callable(unless) and unless():
return
@ -407,6 +410,13 @@ class PreemptiveCache(object):
yield
self._log_access = True
@contextlib.contextmanager
def cache_environment(self, environment):
with self._environment_lock:
self.environment = environment
yield
self.environment = None
def clean_all_data(self, cleanup_function):
assert callable(cleanup_function)
@ -585,13 +595,13 @@ def conditional(condition, met):
def check_etag(etag):
return flask.request.method in ("GET", "HEAD") and \
flask.request.if_none_match and \
flask.request.if_none_match is not None and \
etag in flask.request.if_none_match
def check_lastmodified(lastmodified):
return flask.request.method in ("GET", "HEAD") and \
flask.request.if_modified_since and \
flask.request.if_modified_since is not None and \
lastmodified >= flask.request.if_modified_since

View file

@ -37,6 +37,8 @@ _valid_div_re = re.compile("[a-zA-Z_-]+")
def index():
global _templates, _plugin_names, _plugin_vars
preemptive_cache_enabled = settings().getBoolean(["devel", "cache", "preemptive"])
# helper to check if wizards are active
def wizard_active(templates):
return templates is not None and bool(templates["wizard"]["order"])
@ -116,11 +118,13 @@ def index():
files = collect_files()
lastmodified = compute_lastmodified(files)
lastmodified_ok = util.flask.check_lastmodified(lastmodified)
etag_ok = util.flask.check_etag(compute_etag(files, lastmodified))
etag_ok = util.flask.check_etag(compute_etag(files=files,
lastmodified=lastmodified,
additional=cache_key()))
return lastmodified_ok and etag_ok
def validate_cache(cached):
etag_different = compute_etag() != cached.get_etag()[0]
etag_different = compute_etag(additional=cache_key()) != cached.get_etag()[0]
return force_refresh or etag_different
def collect_files():
@ -162,7 +166,7 @@ def index():
files = collect_files()
return _compute_date(files)
def compute_etag(files=None, lastmodified=None):
def compute_etag(files=None, lastmodified=None, additional=None):
if callable(custom_etag):
try:
etag = custom_etag()
@ -178,6 +182,8 @@ def index():
if lastmodified and not isinstance(lastmodified, basestring):
from werkzeug.http import http_date
lastmodified = http_date(lastmodified)
if additional is None:
additional = []
import hashlib
hash = hashlib.sha1()
@ -186,11 +192,13 @@ def index():
hash.update(",".join(sorted(files)))
if lastmodified:
hash.update(lastmodified)
for add in additional:
hash.update(add)
return hash.hexdigest()
decorated_view = view
decorated_view = util.flask.lastmodified(lambda _: compute_lastmodified())(decorated_view)
decorated_view = util.flask.etagged(lambda _: compute_etag())(decorated_view)
decorated_view = util.flask.etagged(lambda _: compute_etag(additional=cache_key()))(decorated_view)
decorated_view = util.flask.cached(timeout=-1,
refreshif=validate_cache,
key=cache_key,
@ -198,8 +206,7 @@ def index():
decorated_view = util.flask.conditional(check_etag_and_lastmodified, NOT_MODIFIED)(decorated_view)
return decorated_view
ui_plugins = pluginManager.get_implementations(octoprint.plugin.UiPlugin, sorting_context="UiPlugin.on_ui_render")
for plugin in ui_plugins:
def plugin_view(plugin):
if plugin.will_handle_ui(request):
# plugin claims responsibility, let it render the UI
cached = get_cached_view(plugin._identifier,
@ -210,17 +217,18 @@ def index():
custom_etag=plugin.get_ui_custom_etag,
custom_lastmodified=plugin.get_ui_custom_lastmodified)
preemptively_cached = get_preemptively_cached_view(plugin._identifier,
cached,
plugin.get_ui_data_for_preemptive_caching,
plugin.get_ui_additional_request_data_for_preemptive_caching,
plugin.get_ui_additional_unless)
if preemptive_cache_enabled and plugin.get_ui_preemptive_caching_enabled():
view = get_preemptively_cached_view(plugin._identifier,
cached,
plugin.get_ui_data_for_preemptive_caching,
plugin.get_ui_additional_request_data_for_preemptive_caching,
plugin.get_ui_additional_unless)
else:
view = cached
response = preemptively_cached(now, request, render_kwargs)
if response is not None:
break
return view(now, request, render_kwargs)
else:
def default_view():
wizard = wizard_active(_templates)
enable_accesscontrol = userManager.enabled
accesscontrol_active = enable_accesscontrol and userManager.hasBeenCustomized()
@ -250,7 +258,36 @@ def index():
cached,
dict(),
dict())
response = preemptively_cached()
return preemptively_cached()
forced_view = None
preemptive_cache_environment = preemptiveCache.environment
if preemptive_cache_environment is not None and isinstance(preemptive_cache_environment, dict):
forced_view = preemptive_cache_environment.get("plugin", "_default")
if forced_view:
# we have view forced by the preemptive cache
_logger.debug("Forcing rendering of view {}".format(forced_view))
response = None
if forced_view != "_default":
plugin = pluginManager.get_plugin_info(forced_view, require_enabled=True)
if plugin is not None and isinstance(plugin.implementation, octoprint.plugin.UiPlugin):
response = plugin_view(plugin.implementation)
if response is None:
return default_view()
else:
# select view from plugins and fall back on default view if no plugin will handle it
ui_plugins = pluginManager.get_implementations(octoprint.plugin.UiPlugin, sorting_context="UiPlugin.on_ui_render")
for plugin in ui_plugins:
identifier = plugin._identifier
if plugin.will_handle_ui(request):
response = plugin_view(plugin)
if response is not None:
break
else:
response = default_view()
return response

View file

@ -529,6 +529,7 @@ function showConfirmationDialog(msg, onacknowledge, options) {
var proceed = options.proceed || gettext("Proceed");
var proceedClass = options.proceedClass || "danger";
var onproceed = options.onproceed || undefined;
var dialogClass = options.dialogClass || "";
var modalHeader = $('<a href="javascript:void(0)" class="close" data-dismiss="modal" aria-hidden="true">&times;</a><h3>' + title + '</h3>');
var modalBody = $('<p>' + message + '</p><p>' + question + '</p>');
@ -541,6 +542,7 @@ function showConfirmationDialog(msg, onacknowledge, options) {
var modal = $('<div></div>')
.addClass('modal hide fade')
.addClass(dialogClass)
.append($('<div></div>').addClass('modal-header').append(modalHeader))
.append($('<div></div>').addClass('modal-body').append(modalBody))
.append($('<div></div>').addClass('modal-footer').append(cancelButton).append(proceedButton));