From 61934376364f67bbc50ec68be0244b9f8a84a6a5 Mon Sep 17 00:00:00 2001 From: Ellwood Zwovic Date: Tue, 18 Jul 2017 15:10:11 -0700 Subject: [PATCH] Some odd tweaks and repo management code --- bpkg/__init__.py | 109 +++++++++++++++++++++++++++++++++++++++++------ bpkg/subproc.py | 30 ++++++++++--- 2 files changed, 118 insertions(+), 21 deletions(-) diff --git a/bpkg/__init__.py b/bpkg/__init__.py index 2179722..50b990b 100644 --- a/bpkg/__init__.py +++ b/bpkg/__init__.py @@ -126,7 +126,7 @@ class SubprocMixin: def handle_received_data(self): recvd = self.pipe_blender.recv() - self.log.info('Received message from subprocess: %s', recvd) + self.log.debug('Received message from subprocess: %s', recvd) try: handler = self.msg_handlers[type(recvd)] except KeyError: @@ -261,7 +261,7 @@ class BPKG_OT_refresh(SubprocMixin, bpy.types.Operator): import pathlib - storage_path = pathlib.Path(bpy.utils.user_resource('CONFIG', 'addons', create=True)) + storage_path = pathlib.Path(bpy.utils.user_resource('CONFIG', 'packages', create=True)) repository_url = bpy.context.user_preferences.addons[__package__].preferences.repository_url proc = multiprocessing.Process(target=subproc.refresh, @@ -326,6 +326,68 @@ class BPKG_OT_hang(SubprocMixin, bpy.types.Operator): def report_process_died(self): self.report({'ERROR'}, 'Process died, exit code %s' % self.process.exitcode) +class BPKG_OT_load_repositories(SubprocMixin, bpy.types.Operator): + bl_idname = 'bpkg.load_repositories' + bl_label = 'Load Repositories' + bl_description = 'Load repositories from disk' + bl_options = {'REGISTER'} + + log = logging.getLogger(__name__ + '.BPKG_OT_load_repositories') + + def create_subprocess(self): + """ + Start the load process and register message handlers + """ + + import multiprocessing + import pathlib + + # TODO: We need other paths besides this one on subprocess end, so it might be better to pass them all at once. + # For now, just pass this one. + storage_path = pathlib.Path(bpy.utils.user_resource('CONFIG', 'packages', create=True)) + self.log.debug("Using %s as install path", install_path) + + import addon_utils + + proc = multiprocessing.Process( + target=subproc.load_repositories, + args=(self.pipe_subproc, self.storage_path) + ) + return proc + + self.msg_handlers = { + subproc.SubprocError: self._subproc_error, + subproc.RepositoryResult: self._subproc_repository_result, + subproc.Success: self._subproc_success, + subproc.Aborted: self._subproc_aborted, + } + + + def _subproc_error(self, error: subproc.SubprocError): + self.report({'ERROR'}, 'Failed to load repositories: %s' % error.message) + self.quit() + + def _subproc_repository_result(self, result: subproc.RepositoryResult): + bpy.context.user_preferences.addons[__package__].preferences['repo'] = result.repository + self.log.info("Loaded repository %s", result.repository.name) + + def _subproc_success(self, success: subproc.Success): + self.log.info("Successfully loaded repositories") + self.quit() + + def _subproc_aborted(self, aborted: subproc.Aborted): + self.report({'ERROR'}, 'Package installation aborted per your request') + self.quit() + + def report_process_died(self): + if self.process.exitcode: + self.log.error('Process died without telling us! Exit code was %i', self.process.exitcode) + self.report({'ERROR'}, 'Error downloading package, exit code %i' % self.process.exitcode) + else: + self.log.error('Process died without telling us! Exit code was 0 though') + self.report({'WARNING'}, 'Error downloading package, but process finished OK. This is weird.') + + class USERPREF_PT_packages(bpy.types.Panel): bl_label = "Package Management" bl_space_type = 'USER_PREFERENCES' @@ -340,12 +402,6 @@ class USERPREF_PT_packages(bpy.types.Panel): return (userpref.active_section == 'PACKAGES') def draw(self, context): - try: - repo = context.user_preferences.addons[__package__].preferences['repo'] - except KeyError: - # HACK: - # If no repositories are initialized, we should try to refresh them. If that doesn't work, display a message - repo = {'packages': []} layout = self.layout @@ -365,7 +421,7 @@ class USERPREF_PT_packages(bpy.types.Panel): #TODO: more advanced filter/sorting; sort matches which match the filter string from the start higher #Also some caching of this would be nice, this only needs to be re-run when any of the filters change. - def filter_package(package): + def filter_package(package):# {{{ """Returns true if the given package matches all filters""" filterstr = bpy.context.window_manager.package_search category = bpy.context.window_manager.addon_filter @@ -390,9 +446,9 @@ class USERPREF_PT_packages(bpy.types.Panel): if match_search() and match_category(): return True - return False + return False# }}} - def draw_package(pkg, layout): + def draw_package(pkg, layout):# {{{ """Draws the given package""" pkgbox = layout.box() spl = pkgbox.split(.8) @@ -461,12 +517,35 @@ class USERPREF_PT_packages(bpy.types.Panel): spl.label("{}:".format(prop.title())) spl.label(str(blinfo[prop])) - - if pkg.get('expand'): expanded() else: - collapsed() + collapsed()# }}} + + def center_message(layout, msg: str): + """draw a label in the center of an extra-tall row""" + row = layout.row() + row.label(text=msg) + row.alignment='CENTER' + row.scale_y = 10 + + try: + #TODO either store repos in windowmanager and reload from disk every time, or store them in the .blend. Not both + repo = context.user_preferences.addons[__package__].preferences['repo'] + except KeyError: + center_message("Loading Repositories...") + + import pathlib + # TODO: read repository synchronously for now; can't run an operator to do async monitoring from draw code + prefs = bpy.context.user_preferences.addons[__package__].preferences + storage_path = pathlib.Path(bpy.utils.user_resource('CONFIG', 'packages', create=True)) + res = subproc._load_repo(storage_path) + prefs['repo'] = res.to_dict(sort=True, ids=True) + return + + if repo is None: + center_message("No repository found. Add one in the addon preferences.") + return for pkg in repo['packages']: @@ -527,6 +606,7 @@ class PackageManagerPreferences(bpy.types.AddonPreferences): def register(): bpy.utils.register_class(BPKG_OT_install) bpy.utils.register_class(BPKG_OT_refresh) + bpy.utils.register_class(BPKG_OT_load_repositories) bpy.utils.register_class(BPKG_OT_hang) bpy.utils.register_class(USERPREF_PT_packages) bpy.utils.register_class(WM_OT_package_toggle_expand) @@ -549,6 +629,7 @@ def register(): def unregister(): bpy.utils.unregister_class(BPKG_OT_install) bpy.utils.unregister_class(BPKG_OT_refresh) + bpy.utils.unregister_class(BPKG_OT_load_repositories) bpy.utils.unregister_class(BPKG_OT_hang) bpy.utils.unregister_class(USERPREF_PT_packages) bpy.utils.unregister_class(WM_OT_package_toggle_expand) diff --git a/bpkg/subproc.py b/bpkg/subproc.py index 18ee0bd..64d739b 100644 --- a/bpkg/subproc.py +++ b/bpkg/subproc.py @@ -178,8 +178,6 @@ class Repository: if url is None: url = "" self.set_from_dict({'url': url}) - self.log.debug("Initializing repository: %s", self.to_dict()) - self.log.debug("Own URL is '%s'", self.url) # def cleanse_packagelist(self): # """Remove empty packages (no bl_info), packages with no name""" @@ -255,7 +253,6 @@ class Repository: if ids: for pkg in packages: - self.log.debug(pkg['url'], pkg['bl_info']['name'], self.name, self.url) # hash may be too big for a C int pkg['id'] = str(hash(pkg['url'] + pkg['bl_info']['name'] + self.name + self.url)) @@ -389,6 +386,22 @@ def _download(pipe_to_blender, package_url: str, download_dir: pathlib.Path) -> return local_fpath +def _add_to_installed(storage_path: pathlib.Path, pkg: Package): + """Add pkg to local repository""" + repo_path = storage_path / 'local.json' + if repo_path.exists(): + repo = Repository.from_file(repo_path) + else: + repo = Repository() + repo.packages.append(pkg) + repo.to_file(repo_path) + +def _remove_from_installed(storage_path: pathlib.Path, pkg: Package): + """Remove pkg from local repository""" + repo = Repository.from_file(storage_path / 'local.json') + #TODO: this won't work, compare by name? (watch out for conflicts though) + repo.packages.remove(pkg) + def _install(pipe_to_blender, pkgpath: pathlib.Path, dest: pathlib.Path, searchpaths: list): """Extracts/moves package at `pkgpath` to `dest`""" import zipfile @@ -466,7 +479,7 @@ def _install(pipe_to_blender, pkgpath: pathlib.Path, dest: pathlib.Path, searchp return -def download_and_install(pipe_to_blender, package_url: str, install_path: pathlib.Path, search_paths: list): +def download_and_install(pipe_to_blender, package: dict, install_path: pathlib.Path, repo_path: pathlib.Path, search_paths: list): """Downloads and installs the given package.""" from . import cache @@ -474,16 +487,15 @@ def download_and_install(pipe_to_blender, package_url: str, install_path: pathli log = logging.getLogger('%s.download_and_install' % __name__) cache_dir = cache.cache_directory('downloads') - downloaded = _download(pipe_to_blender, package_url, cache_dir) + downloaded = _download(pipe_to_blender, package.url, cache_dir) if not downloaded: log.debug('Download failed/aborted, not going to install anything.') return - # Only send success if _install doesn't throw an exception - # Maybe not the best way to do this? (will also catch exceptions which occur during message sending) try: _install(pipe_to_blender, downloaded, install_path, search_paths) + _add_to_installed(repo_path, Package.from_dict(package)) pipe_to_blender.send(Success()) except InstallException as err: log.exception("Failed to install package: %s", err) @@ -526,9 +538,13 @@ def load(pipe_to_blender, storage_path: pathlib.Path): try: repo = _load_repo(storage_path) pipe_to_blender.send(RepositoryResult(repo.to_dict(sort=True, ids=True))) + pipe_to_blender.send(Success()) + return repo except BadRepository as err: pipe_to_blender.send(SubprocError("Failed to read repository: %s" % err)) +# def load_local(pipe_to_blender + def debug_hang(): """Hangs for an hour. For testing purposes only."""