mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor(runtime): activate managed resources on demand (#6334)
This commit is contained in:
@@ -9,6 +9,10 @@ from urllib.parse import urlparse
|
||||
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.log import logger
|
||||
from app.runtime.managed_resources import (
|
||||
acquire_managed_resource,
|
||||
acquire_managed_resource_async,
|
||||
)
|
||||
from app.adapters.network.http import RequestUtils, cookie_parse
|
||||
|
||||
|
||||
@@ -117,6 +121,47 @@ class BrowserPage(Protocol):
|
||||
...
|
||||
|
||||
|
||||
def launch_browser_context(headless: bool = True, **kwargs: Any) -> BrowserContext:
|
||||
"""
|
||||
启动同步浏览器上下文;有界面模式先显式获取宿主显示资源。
|
||||
|
||||
:param headless: 是否使用无头模式
|
||||
:param kwargs: 浏览器实现接受的其余启动参数
|
||||
:return: 浏览器上下文
|
||||
"""
|
||||
if not headless:
|
||||
acquire_managed_resource(
|
||||
"host.display",
|
||||
reason="headed_browser_launch",
|
||||
retry=True,
|
||||
)
|
||||
from cloakbrowser import launch_context
|
||||
|
||||
return launch_context(headless=headless, **kwargs)
|
||||
|
||||
|
||||
async def launch_browser_context_async(
|
||||
headless: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""
|
||||
启动异步浏览器上下文;有界面模式等待宿主显示资源就绪。
|
||||
|
||||
:param headless: 是否使用无头模式
|
||||
:param kwargs: 浏览器实现接受的其余启动参数
|
||||
:return: 浏览器上下文
|
||||
"""
|
||||
if not headless:
|
||||
await acquire_managed_resource_async(
|
||||
"host.display",
|
||||
reason="headed_browser_launch",
|
||||
retry=True,
|
||||
)
|
||||
from cloakbrowser import launch_context_async
|
||||
|
||||
return await launch_context_async(headless=headless, **kwargs)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _BrowserSessionState:
|
||||
"""保存一个可复用浏览器上下文及其页面游标。"""
|
||||
@@ -662,10 +707,7 @@ class BrowserSessionHelper:
|
||||
viewport: Optional[dict[str, int]] = None,
|
||||
) -> BrowserContext:
|
||||
"""按宿主反检测配置创建 CloakBrowser 上下文。"""
|
||||
from cloakbrowser import launch_context
|
||||
|
||||
context_kwargs = {
|
||||
"headless": headless,
|
||||
"humanize": settings.CLOAKBROWSER_HUMANIZE,
|
||||
"human_preset": settings.CLOAKBROWSER_HUMAN_PRESET,
|
||||
}
|
||||
@@ -673,7 +715,7 @@ class BrowserSessionHelper:
|
||||
context_kwargs["user_agent"] = user_agent
|
||||
if viewport:
|
||||
context_kwargs["viewport"] = viewport
|
||||
return launch_context(**context_kwargs)
|
||||
return launch_browser_context(headless=headless, **context_kwargs)
|
||||
|
||||
def _get_or_create_session(
|
||||
self,
|
||||
@@ -883,13 +925,11 @@ class PlaywrightHelper:
|
||||
"""
|
||||
启动 CloakBrowser 上下文。
|
||||
"""
|
||||
from cloakbrowser import launch_context
|
||||
|
||||
return launch_context(headless=headless,
|
||||
proxy=proxies,
|
||||
user_agent=user_agent,
|
||||
humanize=settings.CLOAKBROWSER_HUMANIZE,
|
||||
human_preset=settings.CLOAKBROWSER_HUMAN_PRESET)
|
||||
return launch_browser_context(headless=headless,
|
||||
proxy=proxies,
|
||||
user_agent=user_agent,
|
||||
humanize=settings.CLOAKBROWSER_HUMANIZE,
|
||||
human_preset=settings.CLOAKBROWSER_HUMAN_PRESET)
|
||||
|
||||
@staticmethod
|
||||
def __fs_cookie_str(cookies: list) -> str:
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
from pyvirtualdisplay import Display
|
||||
|
||||
from app.runtime.log import logger
|
||||
from app.foundation.singleton import Singleton
|
||||
from app.adapters.system.host import SystemUtils
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class DisplayHelper(metaclass=Singleton):
|
||||
"""在容器环境中管理浏览器所需的虚拟显示。"""
|
||||
|
||||
def __init__(self):
|
||||
"""仅在 Docker 内启动虚拟显示服务。"""
|
||||
self._display = None
|
||||
if not SystemUtils.is_docker():
|
||||
return
|
||||
try:
|
||||
self._display = Display(visible=False, size=(1024, 768), extra_args=[os.environ['DISPLAY']])
|
||||
self._display.start()
|
||||
except Exception as err:
|
||||
logger.error(f"DisplayHelper init error: {str(err)}")
|
||||
|
||||
def stop(self):
|
||||
"""停止已经启动的虚拟显示服务。"""
|
||||
if self._display:
|
||||
logger.info("正在停止虚拟显示...")
|
||||
self._display.stop()
|
||||
logger.info("虚拟显示已停止")
|
||||
@@ -0,0 +1,53 @@
|
||||
"""虚拟显示适配器及旧 DisplayHelper 兼容入口。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib import import_module
|
||||
from typing import Any
|
||||
|
||||
from app.foundation.singleton import Singleton
|
||||
from app.runtime.log import logger
|
||||
from app.runtime.managed_resources import (
|
||||
acquire_managed_resource,
|
||||
stop_managed_resource,
|
||||
)
|
||||
|
||||
|
||||
DISPLAY_CAPABILITY_ID = "host.display"
|
||||
|
||||
|
||||
class DisplayHelper(metaclass=Singleton):
|
||||
"""保留旧构造 API,并把资源所有权委托给 host.display 能力。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""显式构造旧门面时激活虚拟显示,失败保持旧 API 的日志语义。"""
|
||||
try:
|
||||
acquire_managed_resource(
|
||||
DISPLAY_CAPABILITY_ID,
|
||||
reason="legacy_display_helper",
|
||||
retry=True,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error("DisplayHelper init error: %s", error)
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止已激活的虚拟显示;未配置 Runtime 时保持幂等。"""
|
||||
stop_managed_resource(
|
||||
DISPLAY_CAPABILITY_ID,
|
||||
reason="legacy_display_helper_stop",
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["DISPLAY_CAPABILITY_ID", "DisplayHelper", "VirtualDisplayResource"]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
"""按需公开资源实现,普通兼容导入不加载显示后端。"""
|
||||
if name != "VirtualDisplayResource":
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
value = getattr(
|
||||
import_module("app.adapters.system.display.resource"),
|
||||
"VirtualDisplayResource",
|
||||
)
|
||||
globals()[name] = value
|
||||
return value
|
||||
@@ -0,0 +1,12 @@
|
||||
schema_version = 1
|
||||
id = "host.display"
|
||||
kind = "managed_resource.sync"
|
||||
entrypoint = "app.adapters.system.display.resource:VirtualDisplayResource"
|
||||
depends_on = []
|
||||
|
||||
[metadata]
|
||||
name = "Virtual Display"
|
||||
|
||||
[activation]
|
||||
policy = "on_first_use"
|
||||
watch = []
|
||||
@@ -0,0 +1,45 @@
|
||||
"""虚拟显示进程的托管资源实现。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.adapters.system.host import SystemUtils
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
class VirtualDisplayResource:
|
||||
"""按需拥有一个容器内虚拟显示进程。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._display: Optional[Any] = None
|
||||
|
||||
@property
|
||||
def display(self) -> Optional[Any]:
|
||||
"""返回当前拥有的显示对象;未启动或已停止时为 None。"""
|
||||
return self._display
|
||||
|
||||
def start(self) -> None:
|
||||
"""仅在容器环境启动虚拟显示,重复启动保持幂等。"""
|
||||
if self._display is not None or not SystemUtils.is_docker():
|
||||
return
|
||||
from pyvirtualdisplay import Display
|
||||
|
||||
display = Display(
|
||||
visible=False,
|
||||
size=(1024, 768),
|
||||
extra_args=[os.environ["DISPLAY"]],
|
||||
)
|
||||
self._display = display
|
||||
display.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止当前资源拥有的显示进程,失败时保留句柄供 Runtime 重试。"""
|
||||
display = self._display
|
||||
if display is None:
|
||||
return
|
||||
logger.info("正在停止虚拟显示...")
|
||||
display.stop()
|
||||
self._display = None
|
||||
logger.info("虚拟显示已停止")
|
||||
Reference in New Issue
Block a user