mirror of
https://github.com/bombsquad-community/plugin-manager.git
synced 2026-08-15 13:03:08 +00:00
Merge 06886ff232 into d442c44ea8
This commit is contained in:
commit
f4b4d802b0
5 changed files with 105 additions and 41 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue