From cd7ac032f454f3182e9bd1aa7b45dc041e0e13fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 9 Sep 2015 16:13:10 +0200 Subject: [PATCH 1/7] Fixed an issue that cause user sessions to not be properly associated Sessions could get duplicated, wrongly saved etc. The reason was not persisting the actual user object to the internal session map (but the LocalProxy instead). That could lead to multiple sessions being created for one login, or the session user being set to an anonymous user, or various other odd effects depending on timing. (cherry picked from commit 8aeac51) --- src/octoprint/server/util/flask.py | 6 ++++-- src/octoprint/users.py | 24 +++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index 30136222..b67a0908 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -227,8 +227,10 @@ def passive_login(): user = flask.ext.login.current_user if user is not None and not user.is_anonymous(): - flask.g.user = user flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id())) + if hasattr(user, "get_session"): + flask.session["usersession.id"] = user.get_session() + flask.g.user = user return flask.jsonify(user.asDict()) elif settings().getBoolean(["accessControl", "autologinLocal"]) \ and settings().get(["accessControl", "autologinAs"]) is not None \ @@ -252,7 +254,7 @@ def passive_login(): logger = logging.getLogger(__name__) logger.exception("Could not autologin user %s for networks %r" % (autologinAs, localNetworks)) - return ("", 204) + return "", 204 #~~ cache decorator for cacheable views diff --git a/src/octoprint/users.py b/src/octoprint/users.py index fc5be2b7..7c40d2d8 100644 --- a/src/octoprint/users.py +++ b/src/octoprint/users.py @@ -28,13 +28,18 @@ class UserManager(object): def login_user(self, user): self._cleanup_sessions() - if user is None \ - or (isinstance(user, LocalProxy) and not isinstance(user._get_current_object(), User)) \ - or (not isinstance(user, LocalProxy) and not isinstance(user, User)): + if user is None: + return + + if isinstance(user, LocalProxy): + user = user._get_current_object() + + if not isinstance(user, User): return None if not isinstance(user, SessionUser): user = SessionUser(user) + self._session_users_by_session[user.get_session()] = user if not user.get_name() in self._session_users_by_username: @@ -49,6 +54,9 @@ class UserManager(object): if user is None: return + if isinstance(user, LocalProxy): + user = user._get_current_object() + if not isinstance(user, SessionUser): return @@ -146,12 +154,10 @@ class UserManager(object): del self._session_users_by_username[username] def findUser(self, username=None, session=None): - if session is not None: - for session in self._session_users_by_session: - user = self._session_users_by_session[session] - if username is None or username == user.get_name(): - return user - break + if session is not None and session in self._session_users_by_session: + user = self._session_users_by_session[session] + if username is None or username == user.get_id(): + return user return None From 5c9b507cb7985f2adae3c822b390a7ef4982efbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 11 Sep 2015 08:34:05 +0200 Subject: [PATCH 2/7] User user id, not user name, for all user operations (cherry picked from commit 7021b9f) --- src/octoprint/server/__init__.py | 6 ++--- src/octoprint/users.py | 40 +++++++++++++++++--------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 18ea638d..646e573d 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -81,7 +81,7 @@ def on_identity_loaded(sender, identity): if user is None: return - identity.provides.add(UserNeed(user.get_name())) + identity.provides.add(UserNeed(user.get_id())) if user.is_user(): identity.provides.add(RoleNeed("user")) if user.is_admin(): @@ -98,9 +98,9 @@ def load_user(id): if userManager is not None: if sessionid: - return userManager.findUser(username=id, session=sessionid) + return userManager.findUser(userid=id, session=sessionid) else: - return userManager.findUser(username=id) + return userManager.findUser(userid=id) return users.DummyUser() diff --git a/src/octoprint/users.py b/src/octoprint/users.py index 7c40d2d8..76bf23bd 100644 --- a/src/octoprint/users.py +++ b/src/octoprint/users.py @@ -23,7 +23,7 @@ class UserManager(object): def __init__(self): self._logger = logging.getLogger(__name__) self._session_users_by_session = dict() - self._session_users_by_username = dict() + self._session_users_by_userid = dict() def login_user(self, user): self._cleanup_sessions() @@ -42,9 +42,10 @@ class UserManager(object): self._session_users_by_session[user.get_session()] = user - if not user.get_name() in self._session_users_by_username: - self._session_users_by_username[user.get_name()] = [] - self._session_users_by_username[user.get_name()].append(user) + userid = user.get_id() + if not userid in self._session_users_by_userid: + self._session_users_by_userid[userid] = [] + self._session_users_by_userid[userid].append(user) self._logger.debug("Logged in user: %r" % user) @@ -60,11 +61,12 @@ class UserManager(object): if not isinstance(user, SessionUser): return - if user.get_name() in self._session_users_by_username: - users_by_username = self._session_users_by_username[user.get_name()] - for u in users_by_username: + userid = user.get_id() + if userid in self._session_users_by_userid: + users_by_userid = self._session_users_by_userid[userid] + for u in users_by_userid: if u.get_session() == user.get_session(): - users_by_username.remove(u) + users_by_userid.remove(u) break if user.get_session() in self._session_users_by_session: @@ -145,18 +147,18 @@ class UserManager(object): pass def removeUser(self, username): - if username in self._session_users_by_username: - users = self._session_users_by_username[username] + if username in self._session_users_by_userid: + users = self._session_users_by_userid[username] sessions = [user.get_session() for user in users if isinstance(user, SessionUser)] for session in sessions: if session in self._session_users_by_session: del self._session_users_by_session[session] - del self._session_users_by_username[username] + del self._session_users_by_userid[username] - def findUser(self, username=None, session=None): + def findUser(self, userid=None, session=None): if session is not None and session in self._session_users_by_session: user = self._session_users_by_session[session] - if username is None or username == user.get_id(): + if userid is None or userid == user.get_id(): return user return None @@ -351,16 +353,16 @@ class FilebasedUserManager(UserManager): self._dirty = True self._save() - def findUser(self, username=None, apikey=None, session=None): - user = UserManager.findUser(self, username=username, session=session) + def findUser(self, userid=None, apikey=None, session=None): + user = UserManager.findUser(self, userid=userid, session=session) if user is not None: return user - if username is not None: - if username not in self._users.keys(): + if userid is not None: + if userid not in self._users.keys(): return None - return self._users[username] + return self._users[userid] elif apikey is not None: for user in self._users.values(): @@ -419,7 +421,7 @@ class User(UserMixin): return self._passwordHash == passwordHash def get_id(self): - return self._username + return self.get_name() def get_name(self): return self._username From b56ba6589c9710543d2796c1ce95acd6152b886d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 11 Sep 2015 08:14:35 +0200 Subject: [PATCH 3/7] Ignore update definitions that are lacking the type Caused a KeyError so far, update definitions that are broken like that will now just be ignored instead. Closes #1057 (cherry picked from commit 2efc5c4) --- src/octoprint/plugins/softwareupdate/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index ad1683a1..399a70a1 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -400,14 +400,13 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if not target in check_targets: continue - populated_check = self._populated_check(target, check) - try: + populated_check = self._populated_check(target, check) target_information, target_update_available, target_update_possible = self._get_current_version(target, populated_check, force=force) if target_information is None: target_information = dict() except exceptions.UnknownCheckType: - self._logger.warn("Unknown update check type for %s" % target) + self._logger.warn("Unknown update check type for target {}".format(target)) continue target_information = dict_merge(dict(local=dict(name="unknown", value="unknown"), remote=dict(name="unknown", value="unknown")), target_information) @@ -655,6 +654,9 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, raise exceptions.RestartFailed() def _populated_check(self, target, check): + if not "type" in check: + raise exceptions.UnknownCheckType() + result = dict(check) if target == "octoprint": From 43ca4d8252eda987d1be6bad8b6531558e9f6974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 21 Sep 2015 16:42:11 +0200 Subject: [PATCH 4/7] SWU: Do not overwrite check information again Current version information of OctoPrint from a check definition could be overwritten for checks under certain circumstances. --- src/octoprint/plugins/softwareupdate/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index 399a70a1..bb324b37 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -689,13 +689,6 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if not "type" in check: raise exceptions.ConfigurationInvalid("no check type defined") - if target == "octoprint": - from octoprint._version import get_versions - from flask.ext.babel import gettext - check["displayName"] = gettext("OctoPrint") - check["displayVersion"] = "{octoprint_version}" - check["current"] = get_versions()["version"] - check_type = check["type"] if check_type == "github_release": return version_checks.github_release From 25a4d4b79b3f3f24a95c475fc56d1cf43324d17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sat, 12 Sep 2015 11:09:28 +0200 Subject: [PATCH 5/7] SWU: Track check origins, ignore if from unavailable plugin There was a problem with software update checks configurations stored in config.yaml for which the providing plugin was then removed, since those check definitions then lacked their default values to be merged on whatever was stored in config.yaml, causing incomplete check configurations as a consequence over which the plugin tripped. This patch fixes that in that it tracks which check config keys are provided by plugins and only returns those as the active check configurations that belong to plugins that are still in the system. TODO: This is only half of the solution. Check configurations of plugins that are being uninstalled should be removed from the config if the user decides to remove any settings by the plugin too. We need some adjustments in the lifecycle tracking in order to make this possible however, so for now this must suffice to at least prevent any errors from occuring when incomplete configs are encountered. (cherry picked from commit 8af8b8f) --- .../plugins/softwareupdate/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index bb324b37..a3f7d2ef 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -59,6 +59,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, self._refresh_configured_checks = False self._configured_checks = self._settings.get(["checks"], merged=True) update_check_hooks = self._plugin_manager.get_hooks("octoprint.plugin.softwareupdate.check_config") + check_providers = self._settings.get(["check_providers"], merged=True) for name, hook in update_check_hooks.items(): try: hook_checks = hook() @@ -66,9 +67,23 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, self._logger.exception("Error while retrieving update information from plugin {name}".format(**locals())) else: for key, data in hook_checks.items(): + check_providers[key] = name if key in self._configured_checks: data = dict_merge(data, self._configured_checks[key]) self._configured_checks[key] = data + self._settings.set(["check_providers"], check_providers) + self._settings.save() + + # we only want to process checks that came from plugins for + # which the plugins are still installed and enabled + config_checks = self._settings.get(["checks"]) + plugin_and_not_enabled = lambda k: k in check_providers and \ + not check_providers[k] in self._plugin_manager.enabled_plugins + obsolete_plugin_checks = filter(plugin_and_not_enabled, + config_checks.keys()) + for key in obsolete_plugin_checks: + self._logger.debug("Check for key {} was provided by plugin {} that's no longer available, ignoring it".format(key, check_providers[key])) + del self._configured_checks[key] return self._configured_checks @@ -134,6 +149,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, }, }, "pip_command": None, + "check_providers": {}, "cache_ttl": 24 * 60, } @@ -406,7 +422,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if target_information is None: target_information = dict() except exceptions.UnknownCheckType: - self._logger.warn("Unknown update check type for target {}".format(target)) + self._logger.warn("Unknown update check type for target {}: {}".format(target, check.get("type", ""))) continue target_information = dict_merge(dict(local=dict(name="unknown", value="unknown"), remote=dict(name="unknown", value="unknown")), target_information) From c26515c13d52ab290a275eba4ca492de0dcd8408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 22 Sep 2015 11:35:47 +0200 Subject: [PATCH 6/7] PipCaller: Allow update of used pip command --- src/octoprint/util/pip.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/octoprint/util/pip.py b/src/octoprint/util/pip.py index 980e5735..94b13926 100644 --- a/src/octoprint/util/pip.py +++ b/src/octoprint/util/pip.py @@ -21,14 +21,13 @@ class PipCaller(object): def __init__(self, configured=None): self._logger = logging.getLogger(__name__) - self._configured = configured + self.configured = configured self._command = None self._version = None - self._command, self._version = self._find_pip() + self.trigger_refresh() - self.refresh = False self.on_log_call = lambda *args, **kwargs: None self.on_log_stdout = lambda *args, **kwargs: None self.on_log_stderr = lambda *args, **kwargs: None @@ -57,10 +56,18 @@ class PipCaller(object): def available(self): return self._command is not None + def trigger_refresh(self): + try: + self._command, self._version = self._find_pip() + except: + self._logger.exception("Error while discovering pip command") + self._command = None + self._version = None + self.refresh = False + def execute(self, *args): if self.refresh: - self._command, self._version = self._find_pip() - self.refresh = False + self.trigger_refresh() if self._command is None: raise UnknownPip() @@ -111,7 +118,7 @@ class PipCaller(object): def _find_pip(self): - pip_command = self._configured + pip_command = self.configured pip_version = None if pip_command is None: From 65bc28a03e56275174918f4b25c4c035d84408aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 22 Sep 2015 11:36:57 +0200 Subject: [PATCH 7/7] PMGR: Added configuration dialog and info re used pip binary & version --- .../plugins/pluginmanager/__init__.py | 26 +++++- .../pluginmanager/static/js/pluginmanager.js | 81 +++++++++++++++++-- .../templates/pluginmanager_settings.jinja2 | 56 +++++++++++++ 3 files changed, 155 insertions(+), 8 deletions(-) diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index c7950f48..8390cd8e 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -87,9 +87,17 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, ) def on_settings_save(self, data): + old_pip = self._settings.get(["pip"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) + new_pip = self._settings.get(["pip"]) + self._repository_cache_ttl = self._settings.get_int(["repository_ttl"]) * 60 - self._pip_caller.refresh = True + if old_pip != new_pip: + self._pip_caller.configured = new_pip + try: + self._pip_caller.trigger_refresh() + except: + self._pip_caller ##~~ AssetPlugin @@ -169,7 +177,18 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, if "refresh_repository" in request.values and request.values["refresh_repository"] in valid_boolean_trues: self._repository_available = self._refresh_repository() - return jsonify(plugins=result, repository=dict(available=self._repository_available, plugins=self._repository_plugins), os=self._get_os(), octoprint=self._get_octoprint_version()) + return jsonify(plugins=result, + repository=dict( + available=self._repository_available, + plugins=self._repository_plugins + ), + os=self._get_os(), + octoprint=self._get_octoprint_version(), + pip=dict( + available=self._pip_caller.available, + command=self._pip_caller.command, + version=str(self._pip_caller.version) + )) def on_api_command(self, command, data): if not admin_permission.can(): @@ -603,7 +622,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, pending_enable=(not plugin.enabled and plugin.key in self._pending_enable), pending_disable=(plugin.enabled and plugin.key in self._pending_disable), pending_install=(plugin.key in self._pending_install), - pending_uninstall=(plugin.key in self._pending_uninstall) + pending_uninstall=(plugin.key in self._pending_uninstall), + origin=plugin.origin.type ) __plugin_name__ = "Plugin Manager" diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 7c5265ff..35a634d8 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -6,6 +6,12 @@ $(function() { self.settingsViewModel = parameters[1]; self.printerState = parameters[2]; + self.config_repositoryUrl = ko.observable(); + self.config_repositoryTtl = ko.observable(); + self.config_pipCommand = ko.observable(); + + self.configurationDialog = $("#settings_plugin_pluginmanager_configurationdialog"); + self.plugins = new ItemListHelper( "plugin.pluginmanager.installedplugins", { @@ -72,6 +78,10 @@ $(function() { self.followDependencyLinks = ko.observable(false); + self.pipAvailable = ko.observable(false); + self.pipCommand = ko.observable(); + self.pipVersion = ko.observable(); + self.working = ko.observable(false); self.workingTitle = ko.observable(); self.workingDialog = undefined; @@ -86,11 +96,15 @@ $(function() { }; self.enableUninstall = function(data) { - return self.enableManagement() && !data.bundled && data.key != 'pluginmanager' && !data.pending_uninstall; + return self.enableManagement() + && (data.origin != "entry_point" || self.pipAvailable()) + && !data.bundled + && data.key != 'pluginmanager' + && !data.pending_uninstall; }; self.enableRepoInstall = function(data) { - return self.enableManagement() && self.isCompatible(data); + return self.enableManagement() && self.pipAvailable() && self.isCompatible(data); }; self.invalidUrl = ko.computed(function() { @@ -100,7 +114,7 @@ $(function() { self.enableUrlInstall = ko.computed(function() { var url = self.installUrl(); - return self.enableManagement() && url !== undefined && url.trim() != "" && !self.invalidUrl(); + return self.enableManagement() && self.pipAvailable() && url !== undefined && url.trim() != "" && !self.invalidUrl(); }); self.invalidArchive = ko.computed(function() { @@ -110,7 +124,7 @@ $(function() { self.enableArchiveInstall = ko.computed(function() { var name = self.uploadFilename(); - return self.enableManagement() && name !== undefined && name.trim() != "" && !self.invalidArchive(); + return self.enableManagement() && self.pipAvailable() && name !== undefined && name.trim() != "" && !self.invalidArchive(); }); self.uploadElement.fileupload({ @@ -167,7 +181,8 @@ $(function() { self.fromResponse = function(data) { self._fromPluginsResponse(data.plugins); - self._fromRepositoryResponse(data.repository) + self._fromRepositoryResponse(data.repository); + self._fromPipResponse(data.pip); }; self._fromPluginsResponse = function(data) { @@ -188,6 +203,17 @@ $(function() { } }; + self._fromPipResponse = function(data) { + self.pipAvailable(data.available); + if (data.available) { + self.pipCommand(data.command); + self.pipVersion(data.version); + } else { + self.pipCommand(undefined); + self.pipVersion(undefined); + } + }; + self.requestData = function(includeRepo) { if (!self.loginState.isAdmin()) { return; @@ -343,6 +369,51 @@ $(function() { self.requestData(true); }; + self.showPluginSettings = function() { + self._copyConfig(); + self.configurationDialog.modal(); + }; + + self.savePluginSettings = function() { + var pipCommand = self.config_pipCommand(); + if (pipCommand != undefined && pipCommand.trim() == "") { + pipCommand = null; + } + + var repository = self.config_repositoryUrl(); + if (repository != undefined && repository.trim() == "") { + repository = null; + } + + var repositoryTtl; + try { + repositoryTtl = parseInt(self.config_repositoryTtl()); + } catch (ex) { + repositoryTtl = null; + } + + var data = { + plugins: { + pluginmanager: { + repository: repository, + repository_ttl: repositoryTtl, + pip: pipCommand + } + } + }; + self.settingsViewModel.saveData(data, function() { + self.configurationDialog.modal("hide"); + self._copyConfig(); + self.refreshRepository(); + }); + }; + + self._copyConfig = function() { + self.config_repositoryUrl(self.settingsViewModel.settings.plugins.pluginmanager.repository()); + self.config_repositoryTtl(self.settingsViewModel.settings.plugins.pluginmanager.repository_ttl()); + self.config_pipCommand(self.settingsViewModel.settings.plugins.pluginmanager.pip()); + }; + self.installed = function(data) { return _.includes(self.installedPlugins(), data.id); }; diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index b855dc81..7c00534a 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -4,7 +4,20 @@ {% endmacro %} +{% macro pluginmanager_nopip() %} +
{% trans %} + The pip command could not be detected automatically, + please configure it manually. No installation and uninstallation of plugin + packages is possible while pip is unavailable. +{% endtrans %}
+{% endmacro %} + {{ pluginmanager_printing() }} +{{ pluginmanager_nopip() }} + +
+ +

{{ _('Installed Plugins') }}

@@ -47,6 +60,10 @@ +

+ Using pip at "" (Version ) +

+ + + +