From c54c79d2db001fdbc1e6260d3fb23e18df3a154b Mon Sep 17 00:00:00 2001 From: Kostas Karmas Date: Mon, 10 Feb 2020 12:17:43 +0100 Subject: [PATCH 01/11] Fix the "save to removable drive" not appearing CURA-7203 --- .../RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py index c89bd31e21..8a183c25f4 100644 --- a/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py +++ b/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py @@ -47,7 +47,10 @@ class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): def checkRemovableDrives(self): drives = {} + # The currently available disk drives, e.g.: bitmask = ...1100 <-- ...DCBA bitmask = ctypes.windll.kernel32.GetLogicalDrives() + # Since we are ignoring drives A and B, the bitmask has has to shift twice to the right + bitmask >>= 2 # Check possible drive letters, from C to Z # Note: using ascii_uppercase because we do not want this to change with locale! # Skip A and B, since those drives are typically reserved for floppy disks. From 5beba6050f4d3283a22aab7b0bbef32ffb8c84b8 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 11 Feb 2020 09:52:39 +0100 Subject: [PATCH 02/11] Fix api version checking CURA-7207 --- .../src/CloudSync/CloudPackageChecker.py | 2 +- .../src/CloudSync/SubscribedPackagesModel.py | 22 +++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py index 14db1a992d..f848f818d7 100644 --- a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py +++ b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py @@ -83,7 +83,7 @@ class CloudPackageChecker(QObject): package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages)) if package_discrepancy: self._model.addDiscrepancies(package_discrepancy) - self._model.initialize(subscribed_packages_payload) + self._model.initialize(self._package_manager, subscribed_packages_payload) self._handlePackageDiscrepancies() def _handlePackageDiscrepancies(self) -> None: diff --git a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py index 614d397d91..f29f23b001 100644 --- a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py +++ b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py @@ -2,9 +2,12 @@ # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import Qt, pyqtProperty, pyqtSlot + +from UM.PackageManager import PackageManager from UM.Qt.ListModel import ListModel +from UM.Version import Version + from cura import ApplicationMetadata -from UM.Logger import Logger from typing import List, Dict, Any @@ -46,7 +49,7 @@ class SubscribedPackagesModel(ListModel): def getIncompatiblePackages(self) -> List[str]: return [package["package_id"] for package in self._items if not package["is_compatible"]] - def initialize(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None: + def initialize(self, package_manager: PackageManager, subscribed_packages_payload: List[Dict[str, Any]]) -> None: self._items.clear() for item in subscribed_packages_payload: if item["package_id"] not in self._discrepancies: @@ -59,10 +62,9 @@ class SubscribedPackagesModel(ListModel): "md5_hash": item["md5_hash"], "is_dismissed": False, } - if self._sdk_version not in item["sdk_versions"]: - package.update({"is_compatible": False}) - else: - package.update({"is_compatible": True}) + + package.update({"is_compatible": self._is_any_version_compatible(package_manager, item["sdk_versions"])}) + try: package.update({"icon_url": item["icon_url"]}) except KeyError: # There is no 'icon_url" in the response payload for this package @@ -70,4 +72,10 @@ class SubscribedPackagesModel(ListModel): self._items.append(package) self.setItems(self._items) - + @staticmethod + def _is_any_version_compatible(package_manager: PackageManager, api_versions: [str]) -> bool: + """:return: True when any of the provided api versions is compatible""" + for version in api_versions: + if package_manager.isPackageCompatible(Version(version)): + return True + return False From bcf4bc813886fc05f7989a983137a5b3fb688ecb Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 11 Feb 2020 11:02:32 +0100 Subject: [PATCH 03/11] Simplify _is_any_version_compatible It now literally says: for any version, is it compatible? Contributes to issue CURA-7207. --- .../Toolbox/src/CloudSync/SubscribedPackagesModel.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py index f29f23b001..8e2b4255f0 100644 --- a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py +++ b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py @@ -74,8 +74,9 @@ class SubscribedPackagesModel(ListModel): @staticmethod def _is_any_version_compatible(package_manager: PackageManager, api_versions: [str]) -> bool: - """:return: True when any of the provided api versions is compatible""" - for version in api_versions: - if package_manager.isPackageCompatible(Version(version)): - return True - return False + """ + Check a list of version numbers if any of them applies to our + application. + :return: ``True`` when any of the provided API versions is compatible. + """ + return any(package_manager.isPackageCompatible(Version(version)) for version in api_versions) From 0c42b1fdea9d40c90bafc5875e32d3af1556c9d0 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 11 Feb 2020 11:06:33 +0100 Subject: [PATCH 04/11] Code style: Method names with lower camelcase Contributes to issue CURA-7207. --- plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py index 8e2b4255f0..53a1b0ee71 100644 --- a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py +++ b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py @@ -63,7 +63,7 @@ class SubscribedPackagesModel(ListModel): "is_dismissed": False, } - package.update({"is_compatible": self._is_any_version_compatible(package_manager, item["sdk_versions"])}) + package.update({"is_compatible": self._isAnyVersionCompatible(package_manager, item["sdk_versions"])}) try: package.update({"icon_url": item["icon_url"]}) @@ -73,7 +73,7 @@ class SubscribedPackagesModel(ListModel): self.setItems(self._items) @staticmethod - def _is_any_version_compatible(package_manager: PackageManager, api_versions: [str]) -> bool: + def _isAnyVersionCompatible(package_manager: PackageManager, api_versions: [str]) -> bool: """ Check a list of version numbers if any of them applies to our application. From ea9a9d58b17e93eed22923001069fceafea8a798 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 11 Feb 2020 11:09:02 +0100 Subject: [PATCH 05/11] Fix typing of list of API versions Contributes to issue CURA-7207. --- plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py index 53a1b0ee71..da15c71fdb 100644 --- a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py +++ b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py @@ -73,7 +73,7 @@ class SubscribedPackagesModel(ListModel): self.setItems(self._items) @staticmethod - def _isAnyVersionCompatible(package_manager: PackageManager, api_versions: [str]) -> bool: + def _isAnyVersionCompatible(package_manager: PackageManager, api_versions: List[str]) -> bool: """ Check a list of version numbers if any of them applies to our application. From f78359ae7644751686f66775cd6624445e6165da Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 11 Feb 2020 11:09:23 +0100 Subject: [PATCH 06/11] Fix pylint typing CURA-7207 --- plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py index f29f23b001..5a2738923f 100644 --- a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py +++ b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py @@ -73,7 +73,7 @@ class SubscribedPackagesModel(ListModel): self.setItems(self._items) @staticmethod - def _is_any_version_compatible(package_manager: PackageManager, api_versions: [str]) -> bool: + def _is_any_version_compatible(package_manager: PackageManager, api_versions: List[str]) -> bool: """:return: True when any of the provided api versions is compatible""" for version in api_versions: if package_manager.isPackageCompatible(Version(version)): From a33cfdfba63a906ac44e6a7068845aacafb9be72 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 11 Feb 2020 11:21:28 +0100 Subject: [PATCH 07/11] Simplify package compatibility checking CURA-7207 --- .../Toolbox/src/CloudSync/SubscribedPackagesModel.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py index da15c71fdb..db16c5ea84 100644 --- a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py +++ b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py @@ -63,7 +63,8 @@ class SubscribedPackagesModel(ListModel): "is_dismissed": False, } - package.update({"is_compatible": self._isAnyVersionCompatible(package_manager, item["sdk_versions"])}) + compatible = any(package_manager.isPackageCompatible(Version(version)) for version in item["sdk_versions"]) + package.update({"is_compatible": compatible}) try: package.update({"icon_url": item["icon_url"]}) @@ -71,12 +72,3 @@ class SubscribedPackagesModel(ListModel): package.update({"icon_url": ""}) self._items.append(package) self.setItems(self._items) - - @staticmethod - def _isAnyVersionCompatible(package_manager: PackageManager, api_versions: List[str]) -> bool: - """ - Check a list of version numbers if any of them applies to our - application. - :return: ``True`` when any of the provided API versions is compatible. - """ - return any(package_manager.isPackageCompatible(Version(version)) for version in api_versions) From 70f22d04e43409f72fd167f34d23d9d9426b3cbf Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Feb 2020 11:51:33 +0100 Subject: [PATCH 08/11] Fix setting the environment for Sentry --- cura_app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cura_app.py b/cura_app.py index 5184a5ab8b..a8fe708c5f 100755 --- a/cura_app.py +++ b/cura_app.py @@ -31,10 +31,11 @@ known_args = vars(parser.parse_known_args()[0]) if with_sentry_sdk: sentry_env = "unknown" # Start off with a "IDK" if hasattr(sys, "frozen"): - sentry_env = "production" # A frozen build is a "real" distribution. - elif ApplicationMetadata.CuraVersion == "master": - sentry_env = "development" - elif "beta" in ApplicationMetadata.CuraVersion or "BETA" in ApplicationMetadata.CuraVersion: + sentry_env = "production" # A frozen build has the posibility to be a "real" distribution. + + if ApplicationMetadata.CuraVersion == "master": + sentry_env = "development" # Master is always a development version. + elif ApplicationMetadata.CuraVersion in ["beta", "BETA"]: sentry_env = "beta" try: if ApplicationMetadata.CuraVersion.split(".")[2] == "99": From 72c1b4d10c7ece92a5f8e2ed7dfd6ad67f75f502 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 11 Feb 2020 13:34:17 +0100 Subject: [PATCH 09/11] Fix LicensePresenter: reset state when presenting licenses CURA-7200 --- .../Toolbox/src/CloudSync/LicensePresenter.py | 24 +++++++++++++++++-- .../Toolbox/src/CloudSync/SyncOrchestrator.py | 2 ++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/plugins/Toolbox/src/CloudSync/LicensePresenter.py b/plugins/Toolbox/src/CloudSync/LicensePresenter.py index 5d70fe7d29..778a36fbde 100644 --- a/plugins/Toolbox/src/CloudSync/LicensePresenter.py +++ b/plugins/Toolbox/src/CloudSync/LicensePresenter.py @@ -4,6 +4,7 @@ from typing import Dict, Optional, List, Any from PyQt5.QtCore import QObject, pyqtSlot +from UM.Logger import Logger from UM.PackageManager import PackageManager from UM.Signal import Signal from cura.CuraApplication import CuraApplication @@ -12,12 +13,19 @@ from UM.i18n import i18nCatalog from .LicenseModel import LicenseModel -## Call present() to show a licenseDialog for a set of packages -# licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages class LicensePresenter(QObject): + """Presents licenses for a set of packages for the user to accept or reject. + + Call present() exactly once to show a licenseDialog for a set of packages + Before presenting another set of licenses, create a new instance using resetCopy(). + + licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages. + """ def __init__(self, app: CuraApplication) -> None: super().__init__() + self._presented = False + """Whether present() has been called and state is expected to be initialized""" self._catalog = i18nCatalog("cura") self._dialog = None # type: Optional[QObject] self._package_manager = app.getPackageManager() # type: PackageManager @@ -39,6 +47,10 @@ class LicensePresenter(QObject): # \param plugin_path: Root directory of the Toolbox plugin # \param packages: Dict[package id, file path] def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None: + if self._presented: + Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__) + return + path = os.path.join(plugin_path, self._compatibility_dialog_path) self._initState(packages) @@ -56,6 +68,14 @@ class LicensePresenter(QObject): } self._dialog = self._app.createQmlComponent(path, context_properties) self._presentCurrentPackage() + self._presented = True + + def resetCopy(self) -> "LicensePresenter": + """Clean up and return a new copy with the same settings such as app""" + if self._dialog: + self._dialog.close() + self.licenseAnswers.disconnectAll() + return LicensePresenter(self._app) @pyqtSlot() def onLicenseAccepted(self) -> None: diff --git a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py index c24703bf92..34fdb40903 100644 --- a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py +++ b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py @@ -72,6 +72,8 @@ class SyncOrchestrator(Extension): self._showErrorMessage(message) plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId())) + self._license_presenter = self._license_presenter.resetCopy() + self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers) self._license_presenter.present(plugin_path, success_items) # Called when user has accepted / declined all licenses for the downloaded packages From 769fc8fd3727718721ad82d99d1a5f029d22c7df Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 17 Feb 2020 10:57:24 +0100 Subject: [PATCH 10/11] Fix crashing when material could not be found See CURA-3M in sentry. It most commonly happens with the anycubic, since it had some weird stuff with it suddenly supporting materials. This change will make it so that no crash happens. Old profiles will still have an empty material. This isn't really an issue, since the interface will mark this as an error (prompting users to switch). --- cura/Machines/Models/IntentModel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cura/Machines/Models/IntentModel.py b/cura/Machines/Models/IntentModel.py index da872f1723..951be7ab2d 100644 --- a/cura/Machines/Models/IntentModel.py +++ b/cura/Machines/Models/IntentModel.py @@ -114,7 +114,10 @@ class IntentModel(ListModel): Logger.log("w", "Could not find the variant %s", active_variant_name) continue active_variant_node = machine_node.variants[active_variant_name] - active_material_node = active_variant_node.materials[extruder.material.getMetaDataEntry("base_file")] + active_material_node = active_variant_node.materials.get(extruder.material.getMetaDataEntry("base_file")) + if active_material_node is None: + Logger.log("w", "Could not find the material %s", extruder.material.getMetaDataEntry("base_file")) + continue nodes.add(active_material_node) return nodes From e52dc56a64186812d48ca4b7d7e318ff24401142 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 17 Feb 2020 17:01:11 +0100 Subject: [PATCH 11/11] Prevent crashing on PermissionError when importing http.server (#7095) * Fix PermissionError when importing http.server Instead of crashing, now it'll just not start the server. This means that you won't be able to receive the response from the log-in so you won't be able to log in. The browser gives an error that it can't find the page on localhost. But at least it doesn't crash. Fixes crash CURA-3Q. * Log an error when the HTTP server can't be started Contributes to crash CURA-3Q. * Indicate that types are optional Attempt to fix the typing issue with MyPy. I can't run this locally so the CI server will have to tell me if this fixed it. Contributes to Sentry issue CURA-3Q. --- cura/OAuth2/LocalAuthorizationServer.py | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/cura/OAuth2/LocalAuthorizationServer.py b/cura/OAuth2/LocalAuthorizationServer.py index 780adf54aa..0e4e491e46 100644 --- a/cura/OAuth2/LocalAuthorizationServer.py +++ b/cura/OAuth2/LocalAuthorizationServer.py @@ -1,13 +1,18 @@ -# Copyright (c) 2019 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import threading -from typing import Optional, Callable, Any, TYPE_CHECKING +from typing import Any, Callable, Optional, TYPE_CHECKING from UM.Logger import Logger -from cura.OAuth2.AuthorizationRequestServer import AuthorizationRequestServer -from cura.OAuth2.AuthorizationRequestHandler import AuthorizationRequestHandler +got_server_type = False +try: + from cura.OAuth2.AuthorizationRequestServer import AuthorizationRequestServer + from cura.OAuth2.AuthorizationRequestHandler import AuthorizationRequestHandler + got_server_type = True +except PermissionError: # Bug in http.server: Can't access MIME types. This will prevent the user from logging in. See Sentry bug Cura-3Q. + Logger.error("Can't start a server due to a PermissionError when starting the http.server.") if TYPE_CHECKING: from cura.OAuth2.Models import AuthenticationResponse @@ -50,15 +55,16 @@ class LocalAuthorizationServer: Logger.log("d", "Starting local web server to handle authorization callback on port %s", self._web_server_port) # Create the server and inject the callback and code. - self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port), AuthorizationRequestHandler) - self._web_server.setAuthorizationHelpers(self._auth_helpers) - self._web_server.setAuthorizationCallback(self._auth_state_changed_callback) - self._web_server.setVerificationCode(verification_code) - self._web_server.setState(state) + if got_server_type: + self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port), AuthorizationRequestHandler) + self._web_server.setAuthorizationHelpers(self._auth_helpers) + self._web_server.setAuthorizationCallback(self._auth_state_changed_callback) + self._web_server.setVerificationCode(verification_code) + self._web_server.setState(state) - # Start the server on a new thread. - self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) - self._web_server_thread.start() + # Start the server on a new thread. + self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) + self._web_server_thread.start() ## Stops the web server if it was running. It also does some cleanup. def stop(self) -> None: