Merge branch 'devel' into dev/settingsOverlays
This commit is contained in:
commit
3893b700f6
6 changed files with 139 additions and 24 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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">×</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));
|
||||
|
|
|
|||
Loading…
Reference in a new issue