SyncOrchestrator.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. # Copyright (c) 2022 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import os
  4. from typing import List, Dict, Any, cast
  5. from UM import i18n_catalog
  6. from UM.Extension import Extension
  7. from UM.Logger import Logger
  8. from UM.Message import Message
  9. from UM.PluginRegistry import PluginRegistry
  10. from cura.CuraApplication import CuraApplication
  11. from .CloudPackageChecker import CloudPackageChecker
  12. from .CloudApiClient import CloudApiClient
  13. from .DiscrepanciesPresenter import DiscrepanciesPresenter
  14. from .DownloadPresenter import DownloadPresenter
  15. from .LicensePresenter import LicensePresenter
  16. from .RestartApplicationPresenter import RestartApplicationPresenter
  17. from .SubscribedPackagesModel import SubscribedPackagesModel
  18. class SyncOrchestrator(Extension):
  19. """Orchestrates the synchronizing of packages from the user account to the installed packages
  20. Example flow:
  21. - CloudPackageChecker compares a list of packages the user `subscribed` to in their account
  22. If there are `discrepancies` between the account and locally installed packages, they are emitted
  23. - DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations`
  24. the user selected to be performed
  25. - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed
  26. - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads
  27. - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to
  28. be installed. It emits the `licenseAnswers` signal for accept or declines
  29. - The CloudApiClient removes the declined packages from the account
  30. - The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files.
  31. - The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect
  32. """
  33. def __init__(self, app: CuraApplication) -> None:
  34. super().__init__()
  35. # Differentiate This PluginObject from the Marketplace. self.getId() includes _name.
  36. # getPluginId() will return the same value for The Marketplace extension and this one
  37. self._name = "SyncOrchestrator"
  38. self._package_manager = app.getPackageManager()
  39. # Keep a reference to the CloudApiClient. it watches for installed packages and subscribes to them
  40. self._cloud_api: CloudApiClient = CloudApiClient.getInstance(app)
  41. self._checker: CloudPackageChecker = CloudPackageChecker(app)
  42. self._checker.discrepancies.connect(self._onDiscrepancies)
  43. self._discrepancies_presenter: DiscrepanciesPresenter = DiscrepanciesPresenter(app)
  44. self._discrepancies_presenter.packageMutations.connect(self._onPackageMutations)
  45. self._download_presenter: DownloadPresenter = DownloadPresenter(app)
  46. self._license_presenter: LicensePresenter = LicensePresenter(app)
  47. self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers)
  48. self._restart_presenter = RestartApplicationPresenter(app)
  49. def _onDiscrepancies(self, model: SubscribedPackagesModel) -> None:
  50. plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId()))
  51. self._discrepancies_presenter.present(plugin_path, model)
  52. def _onPackageMutations(self, mutations: SubscribedPackagesModel) -> None:
  53. self._download_presenter = self._download_presenter.resetCopy()
  54. self._download_presenter.done.connect(self._onDownloadFinished)
  55. self._download_presenter.download(mutations)
  56. def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None:
  57. """Called when a set of packages have finished downloading
  58. :param success_items:: Dict[package_id, Dict[str, str]]
  59. :param error_items:: List[package_id]
  60. """
  61. if error_items:
  62. message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items)))
  63. self._showErrorMessage(message)
  64. plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId()))
  65. self._license_presenter = self._license_presenter.resetCopy()
  66. self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers)
  67. self._license_presenter.present(plugin_path, success_items)
  68. # Called when user has accepted / declined all licenses for the downloaded packages
  69. def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None:
  70. has_changes = False # True when at least one package is installed
  71. for item in answers:
  72. if item["accepted"]:
  73. # install and subscribe packages
  74. if not self._package_manager.installPackage(item["package_path"]):
  75. message = "Could not install {}".format(item["package_id"])
  76. self._showErrorMessage(message)
  77. continue
  78. has_changes = True
  79. else:
  80. self._cloud_api.unsubscribe(item["package_id"])
  81. # delete temp file
  82. try:
  83. os.remove(item["package_path"])
  84. except EnvironmentError as e: # File was already removed, no access rights, etc.
  85. Logger.error("Can't delete temporary package file: {err}".format(err = str(e)))
  86. if has_changes:
  87. self._restart_presenter.present()
  88. def _showErrorMessage(self, text: str):
  89. """Logs an error and shows it to the user"""
  90. Logger.error(text)
  91. Message(text, lifetime = 0, message_type = Message.MessageType.ERROR).show()