FreeCAD
281 строка · 11.9 Кб
1# SPDX-License-Identifier: LGPL-2.1-or-later
2# ***************************************************************************
3# * *
4# * Copyright (c) 2022-2023 FreeCAD Project Association *
5# * Copyright (c) 2019 Yorik van Havre <yorik@uncreated.net> *
6# * *
7# * This file is part of FreeCAD. *
8# * *
9# * FreeCAD is free software: you can redistribute it and/or modify it *
10# * under the terms of the GNU Lesser General Public License as *
11# * published by the Free Software Foundation, either version 2.1 of the *
12# * License, or (at your option) any later version. *
13# * *
14# * FreeCAD is distributed in the hope that it will be useful, but *
15# * WITHOUT ANY WARRANTY; without even the implied warranty of *
16# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
17# * Lesser General Public License for more details. *
18# * *
19# * You should have received a copy of the GNU Lesser General Public *
20# * License along with FreeCAD. If not, see *
21# * <https://www.gnu.org/licenses/>. *
22# * *
23# ***************************************************************************
24
25""" Worker thread classes for Addon Manager installation and removal """
26
27# pylint: disable=c-extension-no-member,too-few-public-methods,too-many-instance-attributes
28
29import io
30import os
31import queue
32import shutil
33import subprocess
34import time
35import zipfile
36from typing import Dict, List
37from enum import Enum, auto
38
39from PySide import QtCore
40
41import FreeCAD
42import addonmanager_utilities as utils
43from addonmanager_metadata import MetadataReader
44from Addon import Addon
45import NetworkManager
46
47translate = FreeCAD.Qt.translate
48
49# @package AddonManager_workers
50# \ingroup ADDONMANAGER
51# \brief Multithread workers for the addon manager
52# @{
53
54
55class UpdateMetadataCacheWorker(QtCore.QThread):
56"""Scan through all available packages and see if our local copy of package.xml needs to be
57updated"""
58
59status_message = QtCore.Signal(str)
60progress_made = QtCore.Signal(int, int)
61package_updated = QtCore.Signal(Addon)
62
63class RequestType(Enum):
64"""The type of item being downloaded."""
65
66PACKAGE_XML = auto()
67METADATA_TXT = auto()
68REQUIREMENTS_TXT = auto()
69ICON = auto()
70
71def __init__(self, repos):
72
73QtCore.QThread.__init__(self)
74self.repos = repos
75self.requests: Dict[int, (Addon, UpdateMetadataCacheWorker.RequestType)] = {}
76NetworkManager.AM_NETWORK_MANAGER.completed.connect(self.download_completed)
77self.requests_completed = 0
78self.total_requests = 0
79self.store = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata")
80FreeCAD.Console.PrintLog(f"Storing Addon Manager cache data in {self.store}\n")
81self.updated_repos = set()
82
83def run(self):
84"""Not usually called directly: instead, create an instance and call its
85start() function to spawn a new thread."""
86
87current_thread = QtCore.QThread.currentThread()
88
89for repo in self.repos:
90if not repo.macro and repo.url and utils.recognized_git_location(repo):
91# package.xml
92index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(
93utils.construct_git_url(repo, "package.xml")
94)
95self.requests[index] = (
96repo,
97UpdateMetadataCacheWorker.RequestType.PACKAGE_XML,
98)
99self.total_requests += 1
100
101# metadata.txt
102index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(
103utils.construct_git_url(repo, "metadata.txt")
104)
105self.requests[index] = (
106repo,
107UpdateMetadataCacheWorker.RequestType.METADATA_TXT,
108)
109self.total_requests += 1
110
111# requirements.txt
112index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(
113utils.construct_git_url(repo, "requirements.txt")
114)
115self.requests[index] = (
116repo,
117UpdateMetadataCacheWorker.RequestType.REQUIREMENTS_TXT,
118)
119self.total_requests += 1
120
121while self.requests:
122if current_thread.isInterruptionRequested():
123for request in self.requests:
124NetworkManager.AM_NETWORK_MANAGER.abort(request)
125return
126# 50 ms maximum between checks for interruption
127QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
128
129# This set contains one copy of each of the repos that got some kind of data in
130# this process. For those repos, tell the main Addon Manager code that it needs
131# to update its copy of the repo, and redraw its information.
132for repo in self.updated_repos:
133self.package_updated.emit(repo)
134
135def download_completed(self, index: int, code: int, data: QtCore.QByteArray) -> None:
136"""Callback for handling a completed metadata file download."""
137if index in self.requests:
138self.requests_completed += 1
139self.progress_made.emit(self.requests_completed, self.total_requests)
140request = self.requests.pop(index)
141if code == 200: # HTTP success
142self.updated_repos.add(request[0]) # mark this repo as updated
143if request[1] == UpdateMetadataCacheWorker.RequestType.PACKAGE_XML:
144self.process_package_xml(request[0], data)
145elif request[1] == UpdateMetadataCacheWorker.RequestType.METADATA_TXT:
146self.process_metadata_txt(request[0], data)
147elif request[1] == UpdateMetadataCacheWorker.RequestType.REQUIREMENTS_TXT:
148self.process_requirements_txt(request[0], data)
149elif request[1] == UpdateMetadataCacheWorker.RequestType.ICON:
150self.process_icon(request[0], data)
151
152def process_package_xml(self, repo: Addon, data: QtCore.QByteArray):
153"""Process the package.xml metadata file"""
154repo.repo_type = Addon.Kind.PACKAGE # By definition
155package_cache_directory = os.path.join(self.store, repo.name)
156if not os.path.exists(package_cache_directory):
157os.makedirs(package_cache_directory)
158new_xml_file = os.path.join(package_cache_directory, "package.xml")
159with open(new_xml_file, "wb") as f:
160f.write(data.data())
161metadata = MetadataReader.from_file(new_xml_file)
162repo.set_metadata(metadata)
163FreeCAD.Console.PrintLog(f"Downloaded package.xml for {repo.name}\n")
164self.status_message.emit(
165translate("AddonsInstaller", "Downloaded package.xml for {}").format(repo.name)
166)
167
168# Grab a new copy of the icon as well: we couldn't enqueue this earlier because
169# we didn't know the path to it, which is stored in the package.xml file.
170icon = repo.get_best_icon_relative_path()
171
172icon_url = utils.construct_git_url(repo, icon)
173index = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(icon_url)
174self.requests[index] = (repo, UpdateMetadataCacheWorker.RequestType.ICON)
175self.total_requests += 1
176
177def _decode_data(self, byte_data, addon_name, file_name) -> str:
178"""UTF-8 decode data, and print an error message if that fails"""
179
180# For review and debugging purposes, store the file locally
181package_cache_directory = os.path.join(self.store, addon_name)
182if not os.path.exists(package_cache_directory):
183os.makedirs(package_cache_directory)
184new_xml_file = os.path.join(package_cache_directory, file_name)
185with open(new_xml_file, "wb") as f:
186f.write(byte_data)
187
188f = ""
189try:
190f = byte_data.decode("utf-8")
191except UnicodeDecodeError as e:
192FreeCAD.Console.PrintWarning(
193translate(
194"AddonsInstaller",
195"Failed to decode {} file for Addon '{}'",
196).format(file_name, addon_name)
197+ "\n"
198)
199FreeCAD.Console.PrintWarning(str(e) + "\n")
200FreeCAD.Console.PrintWarning(
201translate(
202"AddonsInstaller",
203"Any dependency information in this file will be ignored",
204)
205+ "\n"
206)
207return f
208
209def process_metadata_txt(self, repo: Addon, data: QtCore.QByteArray):
210"""Process the metadata.txt metadata file"""
211self.status_message.emit(
212translate("AddonsInstaller", "Downloaded metadata.txt for {}").format(repo.display_name)
213)
214
215f = self._decode_data(data.data(), repo.name, "metadata.txt")
216lines = f.splitlines()
217for line in lines:
218if line.startswith("workbenches="):
219depswb = line.split("=")[1].split(",")
220for wb in depswb:
221wb_name = wb.strip()
222if wb_name:
223repo.requires.add(wb_name)
224FreeCAD.Console.PrintLog(
225f"{repo.display_name} requires FreeCAD Addon '{wb_name}'\n"
226)
227
228elif line.startswith("pylibs="):
229depspy = line.split("=")[1].split(",")
230for pl in depspy:
231dep = pl.strip()
232if dep:
233repo.python_requires.add(dep)
234FreeCAD.Console.PrintLog(
235f"{repo.display_name} requires python package '{dep}'\n"
236)
237
238elif line.startswith("optionalpylibs="):
239opspy = line.split("=")[1].split(",")
240for pl in opspy:
241dep = pl.strip()
242if dep:
243repo.python_optional.add(dep)
244FreeCAD.Console.PrintLog(
245f"{repo.display_name} optionally imports python package"
246+ f" '{pl.strip()}'\n"
247)
248
249def process_requirements_txt(self, repo: Addon, data: QtCore.QByteArray):
250"""Process the requirements.txt metadata file"""
251self.status_message.emit(
252translate(
253"AddonsInstaller",
254"Downloaded requirements.txt for {}",
255).format(repo.display_name)
256)
257
258f = self._decode_data(data.data(), repo.name, "requirements.txt")
259lines = f.splitlines()
260for line in lines:
261break_chars = " <>=~!+#"
262package = line
263for n, c in enumerate(line):
264if c in break_chars:
265package = line[:n].strip()
266break
267if package:
268repo.python_requires.add(package)
269
270def process_icon(self, repo: Addon, data: QtCore.QByteArray):
271"""Convert icon data into a valid icon file and store it"""
272self.status_message.emit(
273translate("AddonsInstaller", "Downloaded icon for {}").format(repo.display_name)
274)
275cache_file = repo.get_cached_icon_filename()
276with open(cache_file, "wb") as icon_file:
277icon_file.write(data.data())
278repo.cached_icon_filename = cache_file
279
280
281# @}
282