From dfa13959fdb28aa12a8e2366dbba6a269d8a0e77 Mon Sep 17 00:00:00 2001 From: FluffyPal Date: Sat, 18 Jul 2026 01:12:48 +0800 Subject: [PATCH 1/7] update plugman's async getter; and lil type fixes --- plugin_manager.py | 66 +++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/plugin_manager.py b/plugin_manager.py index 8d3300dd..eb59b227 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -4,6 +4,7 @@ from bauiv1lib import popup, confirm from bauiv1lib.settings.allsettings import AllSettingsWindow +import urllib.error import urllib.request import http.client import socket @@ -12,7 +13,6 @@ import re import os -import sys import copy import asyncio import pathlib @@ -25,7 +25,7 @@ # Modules used for overriding AllSettingsWindow import logging -PLUGIN_MANAGER_VERSION = "1.1.10" +PLUGIN_MANAGER_VERSION = "1.1.11" REPOSITORY_URL = "https://github.com/bombsquad-community/plugin-manager" # Current tag can be changed to "staging" or any other branch in # plugin manager repo for testing purpose. @@ -43,11 +43,9 @@ } PLUGIN_DIRECTORY = _env["python_directory_user"] -# compatibility for older API versions. -if _env.get("build_number", 0) < 22714: - babase._asyncio._g_asyncio_event_loop = babase._asyncio._asyncio_event_loop -loop = babase._asyncio._g_asyncio_event_loop +loop = babase.app.asyncio_loop +pool = babase.app.threadpool open_popups = [] @@ -130,7 +128,7 @@ def send_network_request(request): async def async_send_network_request(request): - response = await loop.run_in_executor(None, send_network_request, request) + response = await loop.run_in_executor(pool, send_network_request, request) return response @@ -160,7 +158,7 @@ def stream_network_response_to_file(request, file, md5sum=None, retries=3): async def async_stream_network_response_to_file(request, file, md5sum=None, retries=3): content = await loop.run_in_executor( - None, + pool, stream_network_response_to_file, request, file, @@ -201,40 +199,45 @@ class DNSBlockWorkaround: _google_dns_cache = {} - def apply(): + @classmethod + def apply(cls): opener = urllib.request.build_opener( - DNSBlockWorkaround._HTTPHandler, - DNSBlockWorkaround._HTTPSHandler, + cls._HTTPHandler, + cls._HTTPSHandler, ) urllib.request.install_opener(opener) - def _resolve_using_google_dns(hostname): + @classmethod + def _resolve_using_google_dns(cls, hostname): response = urllib.request.urlopen(f"https://dns.google/resolve?name={hostname}") response = response.read() response = json.loads(response) resolved_host = response["Answer"][0]["data"] return resolved_host - def _resolve_using_system_dns(hostname): + @classmethod + def _resolve_using_system_dns(cls, hostname): resolved_host = socket.gethostbyname(hostname) return resolved_host - def _resolve_with_workaround(hostname): - resolved_host_from_cache = DNSBlockWorkaround._google_dns_cache.get(hostname) + @classmethod + def _resolve_with_workaround(cls, hostname): + resolved_host_from_cache = cls._google_dns_cache.get(hostname) if resolved_host_from_cache: return resolved_host_from_cache - resolved_host_by_system_dns = DNSBlockWorkaround._resolve_using_system_dns(hostname) + resolved_host_by_system_dns = cls._resolve_using_system_dns(hostname) - if DNSBlockWorkaround._is_blocked(hostname, resolved_host_by_system_dns): - resolved_host = DNSBlockWorkaround._resolve_using_google_dns(hostname) - DNSBlockWorkaround._google_dns_cache[hostname] = resolved_host + if cls._is_blocked(hostname, resolved_host_by_system_dns): + resolved_host = cls._resolve_using_google_dns(hostname) + cls._google_dns_cache[hostname] = resolved_host else: resolved_host = resolved_host_by_system_dns return resolved_host - def _is_blocked(hostname, address): + @classmethod + def _is_blocked(cls, hostname, address): is_blocked = False if hostname == "raw.githubusercontent.com": # Jio's DNS server may be blocking it. @@ -578,7 +581,7 @@ async def get_content(self): if not self.is_installed: raise PluginNotInstalled("Plugin is not available locally.") - self._content = await loop.run_in_executor(None, self._get_content) + self._content = await loop.run_in_executor(pool, self._get_content) return self._content async def get_api_version(self): @@ -659,7 +662,7 @@ async def enable(self): self.save() def load_plugin(self, entry_point): - plugin_class = babase._general.getclass(entry_point, babase.Plugin) + plugin_class = babase.getclass(entry_point, babase.Plugin) loaded_plugin_instance = plugin_class() loaded_plugin_instance.on_app_running() @@ -683,7 +686,7 @@ def set_version(self, version): async def set_content(self, content): if not self._content: - await loop.run_in_executor(None, self._set_content, content) + await loop.run_in_executor(pool, self._set_content, content) self._content = content return self @@ -869,7 +872,7 @@ def __init__(self): self._index = _CACHE.get("index", {}) self._changelog = _CACHE.get("changelog", {}) self.categories = {} - self.module_path = sys.modules[__name__].__file__ + self.module_path = __file__ self._index_setup_in_progress = False self._changelog_setup_in_progress = False @@ -899,7 +902,7 @@ async def setup_index(self): await self.setup_plugin_categories(index) self._index_setup_in_progress = False - async def get_changelog(self) -> list[str, bool]: + async def get_changelog(self) -> tuple[str, bool]: requested = False if not self._changelog: request = urllib.request.Request(CHANGELOG_META.format( @@ -911,7 +914,7 @@ async def get_changelog(self) -> list[str, bool]: response = await async_send_network_request(request) self._changelog = response.read().decode() requested = True - return [self._changelog, requested] + return self._changelog, requested async def setup_changelog(self, version=None) -> None: if version is None: @@ -930,6 +933,7 @@ async def setup_changelog(self, version=None) -> None: released_on = full_changelog[0].split(version)[1].split('\n')[0] matches = re.findall(pattern, full_changelog[0], re.DOTALL) else: + released_on = ' (Not Provided)' matches = None if matches: @@ -938,7 +942,7 @@ async def setup_changelog(self, version=None) -> None: 'info': matches[0].strip() } else: - changelog = {'released_on': ' (Not Provided)', + changelog = {'released_on': released_on, 'info': f"Changelog entry for version {version} not found."} else: changelog = full_changelog[0] @@ -1735,6 +1739,7 @@ def _cancel(self) -> None: _remove_popup(self) bui.containerwidget(edit=self._root_widget, transition='out_scale') + @staticmethod def button(fn): async def asyncio_handler(fn, self, *args, **kwargs): await fn(self, *args, **kwargs) @@ -2260,8 +2265,8 @@ def main_window_should_preserve_selection(self) -> bool: def __init__( self, - transition: str = "in_right", - origin_widget: bui.Widget = None + transition: str | None = "in_right", + origin_widget: bui.Widget | None = None ): self.plugin_manager = PluginManager() self.category_selection_button = None @@ -3419,7 +3424,6 @@ def on_app_running(self) -> None: from bauiv1lib.settings import allsettings allsettings.AllSettingsWindow = NewAllSettingsWindow DNSBlockWorkaround.apply() - asyncio.set_event_loop(babase._asyncio._g_asyncio_event_loop) startup_tasks = StartupTasks() - loop.create_task(startup_tasks.execute()) + loop.create_task(startup_tasks.execute()) \ No newline at end of file From fad2e493e3f4907709b1bd543659c77ff6b69ca7 Mon Sep 17 00:00:00 2001 From: FluffyPal0 <149132594+FluffyPal0@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:14:43 +0000 Subject: [PATCH 2/7] [ci] apply-plugin-metadata-and-formatting --- plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin_manager.py b/plugin_manager.py index eb59b227..47703bce 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -3426,4 +3426,4 @@ def on_app_running(self) -> None: DNSBlockWorkaround.apply() startup_tasks = StartupTasks() - loop.create_task(startup_tasks.execute()) \ No newline at end of file + loop.create_task(startup_tasks.execute()) From 587f0c65d4bab71867cf08a512fc497fd88f7ace Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:25:16 +0530 Subject: [PATCH 3/7] improved networking and threadpool handling --- CHANGELOG.md | 8 ++++++ index.json | 1 + plugin_manager.py | 64 +++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9390c7fd..14e03732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ### 1.1.10 (12-06-2026) +- Switched to babase.app.asyncio_loop and babase.app.threadpool + for concurrent execution +- A new threadpool to avoid blocking the babase.app.threadpool + which is for short parallel tasks +- Added a shutdown task to shutdown our threadpool + +### 1.1.10 (12-06-2026) + - Fix for older bs versions using `EXPORT_CLASS_NAME_SHORTCUTS` import ### 1.1.9 (11-06-2026) diff --git a/index.json b/index.json index 120a1ba7..d04c9473 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,7 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { + "1.1.11": null, "1.1.10": { "api_version": 9, "commit_sha": "a1baa5f", diff --git a/plugin_manager.py b/plugin_manager.py index 47703bce..f130d619 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -17,7 +17,10 @@ import asyncio import pathlib import hashlib +import weakref +import threading import contextlib +import concurrent.futures from typing import override from datetime import datetime @@ -43,10 +46,40 @@ } PLUGIN_DIRECTORY = _env["python_directory_user"] +NETWORK_REQUEST_TIMEOUT = 10 # seconds loop = babase.app.asyncio_loop pool = babase.app.threadpool +# babase.app.threadpool (aka `pool`, also wired up as the asyncio loop's +# default executor) is reserved for short parallel work and warns/starves +# on long-running tasks. Network requests routinely run past that, so they +# get their own small dedicated pool instead. +_network_pool = concurrent.futures.ThreadPoolExecutor( + max_workers=4, + thread_name_prefix="PluginManagerNetwork", +) + + +async def _shutdown_network_pool() -> None: + """Drain _network_pool's workers so none outlive app shutdown. + + ThreadPoolExecutor.shutdown(wait=True) blocks, so it's run on a + throwaway thread and awaited from here rather than blocking the + event loop (other shutdown tasks run concurrently with this one). + In-flight requests are bounded by NETWORK_REQUEST_TIMEOUT, so this + settles quickly. + """ + done = asyncio.Event() + + def _join() -> None: + _network_pool.shutdown(wait=True, cancel_futures=True) + loop.call_soon_threadsafe(done.set) + + threading.Thread(target=_join, daemon=True).start() + await done.wait() + + open_popups = [] @@ -123,17 +156,19 @@ class CategoryMetadataParseError(Exception): pass + + def send_network_request(request): - return urllib.request.urlopen(request) + return urllib.request.urlopen(request, timeout=NETWORK_REQUEST_TIMEOUT) async def async_send_network_request(request): - response = await loop.run_in_executor(pool, send_network_request, request) + response = await loop.run_in_executor(_network_pool, send_network_request, request) return response def stream_network_response_to_file(request, file, md5sum=None, retries=3): - response = urllib.request.urlopen(request) + response = urllib.request.urlopen(request, timeout=NETWORK_REQUEST_TIMEOUT) chunk_size = 16 * 1024 content = b"" with open(file, "wb") as fout: @@ -158,7 +193,7 @@ def stream_network_response_to_file(request, file, md5sum=None, retries=3): async def async_stream_network_response_to_file(request, file, md5sum=None, retries=3): content = await loop.run_in_executor( - pool, + _network_pool, stream_network_response_to_file, request, file, @@ -209,7 +244,10 @@ def apply(cls): @classmethod def _resolve_using_google_dns(cls, hostname): - response = urllib.request.urlopen(f"https://dns.google/resolve?name={hostname}") + response = urllib.request.urlopen( + f"https://dns.google/resolve?name={hostname}", + timeout=NETWORK_REQUEST_TIMEOUT, + ) response = response.read() response = json.loads(response) resolved_host = response["Answer"][0]["data"] @@ -708,14 +746,23 @@ def save(self): class PluginVersion: def __init__(self, plugin, version, tag=CURRENT_TAG): self.number, info = version - self.plugin = plugin + # Plugin already owns its PluginVersions (via `versions`, + # `latest_version`, `latest_compatible_version`); holding a strong + # back-reference here would form a Plugin<->PluginVersion cycle + # that only the cyclic GC can free. A weakref avoids that so + # they're freed by refcounting alone. + self._plugin_ref = weakref.ref(plugin) self.api_version = info["api_version"] self.released_on = info["released_on"] self.commit_sha = info["commit_sha"] self.md5sum = info["md5sum"] - self.download_url = self.plugin.url.format(content_type="raw", tag=tag) - self.view_url = self.plugin.url.format(content_type="blob", tag=tag) + self.download_url = plugin.url.format(content_type="raw", tag=tag) + self.view_url = plugin.url.format(content_type="blob", tag=tag) + + @property + def plugin(self): + return self._plugin_ref() def __eq__(self, plugin_version): return (self.number, self.plugin.name) == (plugin_version.number, @@ -3424,6 +3471,7 @@ def on_app_running(self) -> None: from bauiv1lib.settings import allsettings allsettings.AllSettingsWindow = NewAllSettingsWindow DNSBlockWorkaround.apply() + babase.app.add_shutdown_task(_shutdown_network_pool()) startup_tasks = StartupTasks() loop.create_task(startup_tasks.execute()) From 454eb351946eb28f03db9a4a11320a9cbfbfc322 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:28:26 +0530 Subject: [PATCH 4/7] forgot to update the version and date in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e03732..bdb0dd11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## Plugin Manager (dd-mm-yyyy) -### 1.1.10 (12-06-2026) +### 1.1.11 (09-08-2026) - Switched to babase.app.asyncio_loop and babase.app.threadpool for concurrent execution From a74246d6a6d8588a72300390c00990f267ee0345 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:59:26 +0000 Subject: [PATCH 5/7] [ci] apply-plugin-metadata-and-formatting --- plugin_manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin_manager.py b/plugin_manager.py index f130d619..7a56b74d 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -156,8 +156,6 @@ class CategoryMetadataParseError(Exception): pass - - def send_network_request(request): return urllib.request.urlopen(request, timeout=NETWORK_REQUEST_TIMEOUT) From b0cca24114a51c11883960fc851c5638cd548101 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:59:28 +0000 Subject: [PATCH 6/7] [ci] apply-version-metadata --- index.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.json b/index.json index d04c9473..08954f74 100644 --- a/index.json +++ b/index.json @@ -1,7 +1,12 @@ { "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "versions": { - "1.1.11": null, + "1.1.11": { + "api_version": 9, + "commit_sha": "a74246d", + "released_on": "09-08-2026", + "md5sum": "1dbf57b11602a22196711bdd3ad18fdc" + }, "1.1.10": { "api_version": 9, "commit_sha": "a1baa5f", From 06886ff2327201c2ea37b493103258c12c31dcc5 Mon Sep 17 00:00:00 2001 From: Loup-Garou911XD <90267658+Loup-Garou911XD@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:34:55 +0530 Subject: [PATCH 7/7] update action versions to latest stable releases --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15fe835e..8d53e3de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: token: ${{ secrets.GITHUB_TOKEN }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -28,7 +28,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.12" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00eba471..6d7c4299 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,14 +12,14 @@ jobs: name: Create Release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.12' - name: 'Get Previous tag' - uses: oprypin/find-latest-tag@v1.1.2 + uses: oprypin/find-latest-tag@v1.1.3 with: repository: ${{ github.repository }} releases-only: true # We know that all relevant tags have a GitHub release for them.