This commit is contained in:
Loup 2026-08-09 13:05:26 +00:00 committed by GitHub
commit f4b4d802b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 105 additions and 41 deletions

View file

@ -20,7 +20,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -28,7 +28,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v7
with: with:
python-version: "3.12" python-version: "3.12"

View file

@ -12,14 +12,14 @@ jobs:
name: Create Release name: Create Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v7
with: with:
python-version: '3.12' python-version: '3.12'
- name: 'Get Previous tag' - name: 'Get Previous tag'
uses: oprypin/find-latest-tag@v1.1.2 uses: oprypin/find-latest-tag@v1.1.3
with: with:
repository: ${{ github.repository }} repository: ${{ github.repository }}
releases-only: true # We know that all relevant tags have a GitHub release for them. releases-only: true # We know that all relevant tags have a GitHub release for them.

View file

@ -1,5 +1,13 @@
## Plugin Manager (dd-mm-yyyy) ## Plugin Manager (dd-mm-yyyy)
### 1.1.11 (09-08-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) ### 1.1.10 (12-06-2026)
- Fix for older bs versions using `EXPORT_CLASS_NAME_SHORTCUTS` import - Fix for older bs versions using `EXPORT_CLASS_NAME_SHORTCUTS` import

View file

@ -1,6 +1,12 @@
{ {
"plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py", "plugin_manager_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/{tag}/plugin_manager.py",
"versions": { "versions": {
"1.1.11": {
"api_version": 9,
"commit_sha": "a74246d",
"released_on": "09-08-2026",
"md5sum": "1dbf57b11602a22196711bdd3ad18fdc"
},
"1.1.10": { "1.1.10": {
"api_version": 9, "api_version": 9,
"commit_sha": "a1baa5f", "commit_sha": "a1baa5f",

View file

@ -4,6 +4,7 @@ import bauiv1 as bui
from bauiv1lib import popup, confirm from bauiv1lib import popup, confirm
from bauiv1lib.settings.allsettings import AllSettingsWindow from bauiv1lib.settings.allsettings import AllSettingsWindow
import urllib.error
import urllib.request import urllib.request
import http.client import http.client
import socket import socket
@ -12,12 +13,14 @@ import ssl
import re import re
import os import os
import sys
import copy import copy
import asyncio import asyncio
import pathlib import pathlib
import hashlib import hashlib
import weakref
import threading
import contextlib import contextlib
import concurrent.futures
from typing import override from typing import override
from datetime import datetime from datetime import datetime
@ -25,7 +28,7 @@ from datetime import datetime
# Modules used for overriding AllSettingsWindow # Modules used for overriding AllSettingsWindow
import logging import logging
PLUGIN_MANAGER_VERSION = "1.1.10" PLUGIN_MANAGER_VERSION = "1.1.11"
REPOSITORY_URL = "https://github.com/bombsquad-community/plugin-manager" REPOSITORY_URL = "https://github.com/bombsquad-community/plugin-manager"
# Current tag can be changed to "staging" or any other branch in # Current tag can be changed to "staging" or any other branch in
# plugin manager repo for testing purpose. # plugin manager repo for testing purpose.
@ -43,11 +46,39 @@ HEADERS = {
} }
PLUGIN_DIRECTORY = _env["python_directory_user"] PLUGIN_DIRECTORY = _env["python_directory_user"]
# compatibility for older API versions. NETWORK_REQUEST_TIMEOUT = 10 # seconds
if _env.get("build_number", 0) < 22714:
babase._asyncio._g_asyncio_event_loop = babase._asyncio._asyncio_event_loop 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()
loop = babase._asyncio._g_asyncio_event_loop
open_popups = [] open_popups = []
@ -126,16 +157,16 @@ class CategoryMetadataParseError(Exception):
def send_network_request(request): 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): async def async_send_network_request(request):
response = await loop.run_in_executor(None, send_network_request, request) response = await loop.run_in_executor(_network_pool, send_network_request, request)
return response return response
def stream_network_response_to_file(request, file, md5sum=None, retries=3): 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 chunk_size = 16 * 1024
content = b"" content = b""
with open(file, "wb") as fout: with open(file, "wb") as fout:
@ -160,7 +191,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): async def async_stream_network_response_to_file(request, file, md5sum=None, retries=3):
content = await loop.run_in_executor( content = await loop.run_in_executor(
None, _network_pool,
stream_network_response_to_file, stream_network_response_to_file,
request, request,
file, file,
@ -201,40 +232,48 @@ class DNSBlockWorkaround:
_google_dns_cache = {} _google_dns_cache = {}
def apply(): @classmethod
def apply(cls):
opener = urllib.request.build_opener( opener = urllib.request.build_opener(
DNSBlockWorkaround._HTTPHandler, cls._HTTPHandler,
DNSBlockWorkaround._HTTPSHandler, cls._HTTPSHandler,
) )
urllib.request.install_opener(opener) urllib.request.install_opener(opener)
def _resolve_using_google_dns(hostname): @classmethod
response = urllib.request.urlopen(f"https://dns.google/resolve?name={hostname}") def _resolve_using_google_dns(cls, hostname):
response = urllib.request.urlopen(
f"https://dns.google/resolve?name={hostname}",
timeout=NETWORK_REQUEST_TIMEOUT,
)
response = response.read() response = response.read()
response = json.loads(response) response = json.loads(response)
resolved_host = response["Answer"][0]["data"] resolved_host = response["Answer"][0]["data"]
return resolved_host return resolved_host
def _resolve_using_system_dns(hostname): @classmethod
def _resolve_using_system_dns(cls, hostname):
resolved_host = socket.gethostbyname(hostname) resolved_host = socket.gethostbyname(hostname)
return resolved_host return resolved_host
def _resolve_with_workaround(hostname): @classmethod
resolved_host_from_cache = DNSBlockWorkaround._google_dns_cache.get(hostname) def _resolve_with_workaround(cls, hostname):
resolved_host_from_cache = cls._google_dns_cache.get(hostname)
if resolved_host_from_cache: if resolved_host_from_cache:
return 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): if cls._is_blocked(hostname, resolved_host_by_system_dns):
resolved_host = DNSBlockWorkaround._resolve_using_google_dns(hostname) resolved_host = cls._resolve_using_google_dns(hostname)
DNSBlockWorkaround._google_dns_cache[hostname] = resolved_host cls._google_dns_cache[hostname] = resolved_host
else: else:
resolved_host = resolved_host_by_system_dns resolved_host = resolved_host_by_system_dns
return resolved_host return resolved_host
def _is_blocked(hostname, address): @classmethod
def _is_blocked(cls, hostname, address):
is_blocked = False is_blocked = False
if hostname == "raw.githubusercontent.com": if hostname == "raw.githubusercontent.com":
# Jio's DNS server may be blocking it. # Jio's DNS server may be blocking it.
@ -578,7 +617,7 @@ class PluginLocal:
if not self.is_installed: if not self.is_installed:
raise PluginNotInstalled("Plugin is not available locally.") 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 return self._content
async def get_api_version(self): async def get_api_version(self):
@ -659,7 +698,7 @@ class PluginLocal:
self.save() self.save()
def load_plugin(self, entry_point): 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 = plugin_class()
loaded_plugin_instance.on_app_running() loaded_plugin_instance.on_app_running()
@ -683,7 +722,7 @@ class PluginLocal:
async def set_content(self, content): async def set_content(self, content):
if not 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 self._content = content
return self return self
@ -705,14 +744,23 @@ class PluginLocal:
class PluginVersion: class PluginVersion:
def __init__(self, plugin, version, tag=CURRENT_TAG): def __init__(self, plugin, version, tag=CURRENT_TAG):
self.number, info = version 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.api_version = info["api_version"]
self.released_on = info["released_on"] self.released_on = info["released_on"]
self.commit_sha = info["commit_sha"] self.commit_sha = info["commit_sha"]
self.md5sum = info["md5sum"] self.md5sum = info["md5sum"]
self.download_url = self.plugin.url.format(content_type="raw", tag=tag) self.download_url = plugin.url.format(content_type="raw", tag=tag)
self.view_url = self.plugin.url.format(content_type="blob", 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): def __eq__(self, plugin_version):
return (self.number, self.plugin.name) == (plugin_version.number, return (self.number, self.plugin.name) == (plugin_version.number,
@ -869,7 +917,7 @@ class PluginManager:
self._index = _CACHE.get("index", {}) self._index = _CACHE.get("index", {})
self._changelog = _CACHE.get("changelog", {}) self._changelog = _CACHE.get("changelog", {})
self.categories = {} self.categories = {}
self.module_path = sys.modules[__name__].__file__ self.module_path = __file__
self._index_setup_in_progress = False self._index_setup_in_progress = False
self._changelog_setup_in_progress = False self._changelog_setup_in_progress = False
@ -899,7 +947,7 @@ class PluginManager:
await self.setup_plugin_categories(index) await self.setup_plugin_categories(index)
self._index_setup_in_progress = False self._index_setup_in_progress = False
async def get_changelog(self) -> list[str, bool]: async def get_changelog(self) -> tuple[str, bool]:
requested = False requested = False
if not self._changelog: if not self._changelog:
request = urllib.request.Request(CHANGELOG_META.format( request = urllib.request.Request(CHANGELOG_META.format(
@ -911,7 +959,7 @@ class PluginManager:
response = await async_send_network_request(request) response = await async_send_network_request(request)
self._changelog = response.read().decode() self._changelog = response.read().decode()
requested = True requested = True
return [self._changelog, requested] return self._changelog, requested
async def setup_changelog(self, version=None) -> None: async def setup_changelog(self, version=None) -> None:
if version is None: if version is None:
@ -930,6 +978,7 @@ class PluginManager:
released_on = full_changelog[0].split(version)[1].split('\n')[0] released_on = full_changelog[0].split(version)[1].split('\n')[0]
matches = re.findall(pattern, full_changelog[0], re.DOTALL) matches = re.findall(pattern, full_changelog[0], re.DOTALL)
else: else:
released_on = ' (Not Provided)'
matches = None matches = None
if matches: if matches:
@ -938,7 +987,7 @@ class PluginManager:
'info': matches[0].strip() 'info': matches[0].strip()
} }
else: else:
changelog = {'released_on': ' (Not Provided)', changelog = {'released_on': released_on,
'info': f"Changelog entry for version {version} not found."} 'info': f"Changelog entry for version {version} not found."}
else: else:
changelog = full_changelog[0] changelog = full_changelog[0]
@ -1735,6 +1784,7 @@ class PluginWindow(popup.PopupWindow):
_remove_popup(self) _remove_popup(self)
bui.containerwidget(edit=self._root_widget, transition='out_scale') bui.containerwidget(edit=self._root_widget, transition='out_scale')
@staticmethod
def button(fn): def button(fn):
async def asyncio_handler(fn, self, *args, **kwargs): async def asyncio_handler(fn, self, *args, **kwargs):
await fn(self, *args, **kwargs) await fn(self, *args, **kwargs)
@ -2260,8 +2310,8 @@ class PluginManagerWindow(bui.MainWindow):
def __init__( def __init__(
self, self,
transition: str = "in_right", transition: str | None = "in_right",
origin_widget: bui.Widget = None origin_widget: bui.Widget | None = None
): ):
self.plugin_manager = PluginManager() self.plugin_manager = PluginManager()
self.category_selection_button = None self.category_selection_button = None
@ -3419,7 +3469,7 @@ class EntryPoint(babase.Plugin):
from bauiv1lib.settings import allsettings from bauiv1lib.settings import allsettings
allsettings.AllSettingsWindow = NewAllSettingsWindow allsettings.AllSettingsWindow = NewAllSettingsWindow
DNSBlockWorkaround.apply() DNSBlockWorkaround.apply()
asyncio.set_event_loop(babase._asyncio._g_asyncio_event_loop) babase.app.add_shutdown_task(_shutdown_network_pool())
startup_tasks = StartupTasks() startup_tasks = StartupTasks()
loop.create_task(startup_tasks.execute()) loop.create_task(startup_tasks.execute())