mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
refactor: unify message channel shutdown convergence
This commit is contained in:
@@ -93,10 +93,10 @@ class _ModuleBase(ConfigReloadMixin, metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop(self) -> None:
|
||||
def stop(self) -> Optional[bool]:
|
||||
"""
|
||||
如果关闭时模块有服务需要停止,需要实现此方法
|
||||
:return: None,该方法可被多个模块同时处理
|
||||
:return: False 表示资源尚未收敛;None/True 表示本次停止完成
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -73,6 +73,21 @@ class _MessageChannelModuleBase(_ModuleBase, _MessageBase[TService]):
|
||||
"""
|
||||
return bool(client.get_state()), ""
|
||||
|
||||
def _stop_service_instances(self) -> bool:
|
||||
"""停止全部渠道实例,并聚合客户端返回的资源收敛结果。"""
|
||||
converged = True
|
||||
for client in self.get_instances().values():
|
||||
stop = getattr(client, "stop", None)
|
||||
if not callable(stop):
|
||||
continue
|
||||
try:
|
||||
if stop() is False:
|
||||
converged = False
|
||||
except Exception as err:
|
||||
logger.error("停止%s模块实例失败:%s", self.get_name(), err)
|
||||
converged = False
|
||||
return converged
|
||||
|
||||
def register_commands(self, commands: Dict[str, dict]) -> None:
|
||||
"""
|
||||
注册命令,实现这个函数接收系统可用的命令菜单
|
||||
|
||||
@@ -96,13 +96,9 @@ class DiscordModule(_MessageChannelModuleBase[Discord]):
|
||||
"""
|
||||
return 4
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止模块"""
|
||||
for client in self.get_instances().values():
|
||||
try:
|
||||
client.stop()
|
||||
except Exception as err:
|
||||
logger.error(f"停止Discord模块实例失败:{err}")
|
||||
def stop(self) -> bool:
|
||||
"""停止全部 Discord 实例,并返回资源是否全部收敛。"""
|
||||
return self._stop_service_instances()
|
||||
|
||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||
pass
|
||||
|
||||
@@ -39,6 +39,8 @@ class Discord:
|
||||
"""
|
||||
|
||||
_MAX_SLASH_COMMANDS = 100
|
||||
_client_close_timeout_seconds = 10
|
||||
_thread_join_timeout_seconds = 5
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -247,10 +249,10 @@ class Discord:
|
||||
self._thread = threading.Thread(target=runner, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
"""停止 Discord 客户端,并在关闭事件循环前收口 typing owner。"""
|
||||
def stop(self) -> bool:
|
||||
"""停止 Discord 客户端,并返回事件循环线程是否已经终止。"""
|
||||
if not self._client or not self._loop or not self._thread:
|
||||
return
|
||||
return True
|
||||
self._stop_requested.set()
|
||||
loop = self._loop
|
||||
thread = self._thread
|
||||
@@ -264,19 +266,23 @@ class Discord:
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._client.close(), loop
|
||||
).result(timeout=10)
|
||||
).result(timeout=self._client_close_timeout_seconds)
|
||||
except Exception as err:
|
||||
logger.error(f"关闭 Discord Bot 失败:{err}")
|
||||
self._ready_event.clear()
|
||||
thread.join(timeout=5)
|
||||
if thread is not threading.current_thread():
|
||||
thread.join(timeout=self._thread_join_timeout_seconds)
|
||||
if thread.is_alive():
|
||||
try:
|
||||
loop.call_soon_threadsafe(loop.stop)
|
||||
except Exception as err:
|
||||
logger.error(f"停止 Discord 事件循环失败:{err}")
|
||||
thread.join(timeout=5)
|
||||
if thread is not threading.current_thread():
|
||||
thread.join(timeout=self._thread_join_timeout_seconds)
|
||||
if thread.is_alive():
|
||||
logger.error("Discord Bot 线程未在超时内停止")
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._ready_event.is_set() and self._client is not None
|
||||
|
||||
@@ -48,13 +48,9 @@ class FeishuModule(_MessageChannelModuleBase[Feishu]):
|
||||
"""
|
||||
return False
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止模块"""
|
||||
for client in self.get_instances().values():
|
||||
try:
|
||||
client.stop()
|
||||
except Exception as err:
|
||||
logger.error(f"停止飞书模块实例失败:{err}")
|
||||
def stop(self) -> bool:
|
||||
"""停止全部飞书实例,并返回资源是否全部收敛。"""
|
||||
return self._stop_service_instances()
|
||||
|
||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||
"""通知模块通过系统通知配置控制实例化,这里不额外设置环境开关。"""
|
||||
|
||||
@@ -129,6 +129,8 @@ class Feishu:
|
||||
"""飞书通知客户端,负责长连接收消息与主动发送通知。"""
|
||||
|
||||
PROCESSING_REACTION_EMOJI = "GLANCE"
|
||||
_ws_shutdown_timeout_seconds = 5
|
||||
_ws_join_timeout_seconds = 5
|
||||
STREAM_CARD_TITLE_ELEMENT_ID = "mp_stream_title"
|
||||
STREAM_CARD_BODY_ELEMENT_ID = "mp_stream_body"
|
||||
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".ico", ".tiff", ".heic"}
|
||||
@@ -678,12 +680,13 @@ class Feishu:
|
||||
"""返回飞书客户端是否已就绪。"""
|
||||
return self._ready.is_set() and self._api_client is not None
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止飞书客户端并结束长连接线程。"""
|
||||
def stop(self) -> bool:
|
||||
"""停止飞书客户端,并返回长连接线程是否已经终止。"""
|
||||
self._stop_event.set()
|
||||
self._ready.clear()
|
||||
ws_client = self._ws_client
|
||||
ws_loop = self._ws_loop
|
||||
ws_thread = self._ws_thread
|
||||
if ws_client:
|
||||
try:
|
||||
ws_client._auto_reconnect = False
|
||||
@@ -692,11 +695,19 @@ class Feishu:
|
||||
self._shutdown_ws_client(),
|
||||
ws_loop,
|
||||
)
|
||||
shutdown_future.result(timeout=5)
|
||||
shutdown_future.result(timeout=self._ws_shutdown_timeout_seconds)
|
||||
except Exception as err:
|
||||
logger.debug(f"停止飞书客户端失败:{err}")
|
||||
if self._ws_thread and self._ws_thread.is_alive():
|
||||
self._ws_thread.join(timeout=5)
|
||||
if (
|
||||
ws_thread
|
||||
and ws_thread.is_alive()
|
||||
and ws_thread is not threading.current_thread()
|
||||
):
|
||||
ws_thread.join(timeout=self._ws_join_timeout_seconds)
|
||||
if ws_thread and ws_thread.is_alive():
|
||||
logger.error("飞书长连接线程未在关闭预算内退出")
|
||||
return False
|
||||
return True
|
||||
|
||||
def parse_message(self, body: Any) -> Optional[IncomingMessage]:
|
||||
"""解析飞书转发到消息入口的 JSON 报文。"""
|
||||
|
||||
@@ -90,13 +90,9 @@ class QQBotModule(_MessageChannelModuleBase[QQBot]):
|
||||
"""
|
||||
return False
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止模块"""
|
||||
for client in self.get_instances().values():
|
||||
try:
|
||||
client.stop()
|
||||
except Exception as err:
|
||||
logger.error(f"停止QQ Bot模块实例失败:{err}")
|
||||
def stop(self) -> bool:
|
||||
"""停止全部 QQ Bot 实例,并返回资源是否全部收敛。"""
|
||||
return self._stop_service_instances()
|
||||
|
||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||
pass
|
||||
|
||||
@@ -38,6 +38,8 @@ _MAX_IMAGE_SIZE: Tuple[int, int] = (512, 512)
|
||||
class QQBot:
|
||||
"""QQ Bot 通知客户端"""
|
||||
|
||||
_gateway_join_timeout_seconds = 20
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
QQ_APP_ID: Optional[str] = None,
|
||||
@@ -161,8 +163,8 @@ class QQBot:
|
||||
except Exception as e:
|
||||
logger.error(f"QQ Bot Gateway 启动失败: {e}")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止 Gateway 连接"""
|
||||
def stop(self) -> bool:
|
||||
"""停止 Gateway 连接,并返回后台线程是否已经终止。"""
|
||||
if self._gateway_stop is not None:
|
||||
self._gateway_stop.set()
|
||||
try:
|
||||
@@ -170,12 +172,19 @@ class QQBot:
|
||||
self._gateway_ws_holder[0].close()
|
||||
except Exception as e:
|
||||
logger.debug(f"QQ Bot Gateway WebSocket close: {e}")
|
||||
if self._gateway_thread is not None and self._gateway_thread.is_alive():
|
||||
self._gateway_thread.join(timeout=20)
|
||||
if self._gateway_thread.is_alive():
|
||||
logger.warning(
|
||||
"QQ Bot Gateway 线程在 stop 后仍未退出,可能存在重复收消息,请重启进程"
|
||||
)
|
||||
gateway_thread = self._gateway_thread
|
||||
if (
|
||||
gateway_thread is not None
|
||||
and gateway_thread.is_alive()
|
||||
and gateway_thread is not threading.current_thread()
|
||||
):
|
||||
gateway_thread.join(timeout=self._gateway_join_timeout_seconds)
|
||||
if gateway_thread is not None and gateway_thread.is_alive():
|
||||
logger.warning(
|
||||
"QQ Bot Gateway 线程在 stop 后仍未退出,可能存在重复收消息,请重启进程"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_state(self) -> bool:
|
||||
"""获取就绪状态"""
|
||||
|
||||
@@ -77,13 +77,9 @@ class SlackModule(_MessageChannelModuleBase[Slack]):
|
||||
"""
|
||||
return 3
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止模块"""
|
||||
for client in self.get_instances().values():
|
||||
try:
|
||||
client.stop()
|
||||
except Exception as err:
|
||||
logger.error(f"停止Slack模块实例失败:{err}")
|
||||
def stop(self) -> bool:
|
||||
"""停止全部 Slack 实例,并返回资源是否全部收敛。"""
|
||||
return self._stop_service_instances()
|
||||
|
||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||
pass
|
||||
|
||||
@@ -117,13 +117,16 @@ class Slack:
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
def stop(self):
|
||||
def stop(self) -> bool:
|
||||
"""关闭 Socket Mode 服务,并返回资源是否成功释放。"""
|
||||
if self._service:
|
||||
try:
|
||||
self._service.close()
|
||||
logger.info("Slack消息接收服务已停止")
|
||||
except Exception as err:
|
||||
logger.error("Slack消息接收服务停止失败: %s" % str(err))
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_state(self) -> bool:
|
||||
"""
|
||||
|
||||
@@ -74,15 +74,7 @@ class TelegramModule(_MessageChannelModuleBase[Telegram]):
|
||||
|
||||
def stop(self) -> bool:
|
||||
"""停止全部 Telegram 实例,并返回资源是否全部收敛。"""
|
||||
converged = True
|
||||
for client in self.get_instances().values():
|
||||
try:
|
||||
if client.stop() is False:
|
||||
converged = False
|
||||
except Exception as err:
|
||||
logger.error(f"停止Telegram模块实例失败:{err}")
|
||||
converged = False
|
||||
return converged
|
||||
return self._stop_service_instances()
|
||||
|
||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||
"""
|
||||
|
||||
@@ -73,14 +73,9 @@ class WechatModule(_MessageChannelModuleBase[WeChat]):
|
||||
"""
|
||||
return 1
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止模块"""
|
||||
for client in self.get_instances().values():
|
||||
try:
|
||||
if hasattr(client, "stop"):
|
||||
client.stop()
|
||||
except Exception as err:
|
||||
logger.error(f"停止微信模块实例失败:{err}")
|
||||
def stop(self) -> bool:
|
||||
"""停止全部微信实例,并返回长连接资源是否全部收敛。"""
|
||||
return self._stop_service_instances()
|
||||
|
||||
@staticmethod
|
||||
def _is_bot_mode(config: dict) -> bool:
|
||||
|
||||
@@ -38,6 +38,8 @@ class WeChatBot:
|
||||
_default_ws_url = "wss://openws.work.weixin.qq.com"
|
||||
_heartbeat_interval = 30
|
||||
_ack_timeout = 10
|
||||
_gateway_join_timeout_seconds = 5
|
||||
_heartbeat_join_timeout_seconds = 2
|
||||
|
||||
def __init__(self,
|
||||
WECHAT_BOT_ID: Optional[str] = None,
|
||||
@@ -126,7 +128,8 @@ class WeChatBot:
|
||||
self._heartbeat_thread.start()
|
||||
logger.info(f"企业微信智能机器人长连接已启动:{self._config_name}")
|
||||
|
||||
def stop(self) -> None:
|
||||
def stop(self) -> bool:
|
||||
"""停止网关与心跳线程,并返回两个 owner 是否均已终止。"""
|
||||
self._stop_event.set()
|
||||
self._authenticated.clear()
|
||||
if self._ws_app:
|
||||
@@ -134,10 +137,27 @@ class WeChatBot:
|
||||
self._ws_app.close()
|
||||
except Exception as err:
|
||||
logger.debug(f"关闭企业微信智能机器人连接失败:{err}")
|
||||
if self._ws_thread and self._ws_thread.is_alive():
|
||||
self._ws_thread.join(timeout=5)
|
||||
if self._heartbeat_thread and self._heartbeat_thread.is_alive():
|
||||
self._heartbeat_thread.join(timeout=2)
|
||||
ws_thread = self._ws_thread
|
||||
heartbeat_thread = self._heartbeat_thread
|
||||
if (
|
||||
ws_thread
|
||||
and ws_thread.is_alive()
|
||||
and ws_thread is not threading.current_thread()
|
||||
):
|
||||
ws_thread.join(timeout=self._gateway_join_timeout_seconds)
|
||||
if (
|
||||
heartbeat_thread
|
||||
and heartbeat_thread.is_alive()
|
||||
and heartbeat_thread is not threading.current_thread()
|
||||
):
|
||||
heartbeat_thread.join(timeout=self._heartbeat_join_timeout_seconds)
|
||||
converged = not any(
|
||||
thread and thread.is_alive()
|
||||
for thread in (ws_thread, heartbeat_thread)
|
||||
)
|
||||
if not converged:
|
||||
logger.error("企业微信智能机器人线程未在关闭预算内退出")
|
||||
return converged
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._ready and self._authenticated.is_set()
|
||||
|
||||
@@ -69,13 +69,9 @@ class WechatClawBotModule(_MessageChannelModuleBase[WechatClawBot]):
|
||||
"""
|
||||
return False
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止模块"""
|
||||
for client in self.get_instances().values():
|
||||
try:
|
||||
client.stop()
|
||||
except Exception as err:
|
||||
logger.error(f"停止微信 ClawBot 模块实例失败:{err}")
|
||||
def stop(self) -> bool:
|
||||
"""停止全部微信 ClawBot 实例,并返回资源是否全部收敛。"""
|
||||
return self._stop_service_instances()
|
||||
|
||||
def _test_connection(self, client) -> Tuple[bool, str]:
|
||||
"""微信 ClawBot 的连接探测返回 (状态, 信息)。"""
|
||||
|
||||
@@ -1441,6 +1441,7 @@ class WechatClawBot:
|
||||
_default_base_url = "https://ilinkai.weixin.qq.com"
|
||||
_qrcode_ttl_seconds = 240
|
||||
_active_target_ttl_seconds = 24 * 60 * 60
|
||||
_poll_join_timeout_seconds = 5
|
||||
|
||||
@classmethod
|
||||
def _build_cache_key(cls, config_name: str) -> str:
|
||||
@@ -1706,12 +1707,21 @@ class WechatClawBot:
|
||||
"""获取当前登录状态。"""
|
||||
return bool(self._state.get("bot_token"))
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止消息轮询。"""
|
||||
def stop(self) -> bool:
|
||||
"""停止消息轮询,并保留超时线程 owner 供后续重试。"""
|
||||
self._stop_event.set()
|
||||
if self._poll_thread and self._poll_thread.is_alive():
|
||||
self._poll_thread.join(timeout=5)
|
||||
poll_thread = self._poll_thread
|
||||
if (
|
||||
poll_thread
|
||||
and poll_thread.is_alive()
|
||||
and poll_thread is not threading.current_thread()
|
||||
):
|
||||
poll_thread.join(timeout=self._poll_join_timeout_seconds)
|
||||
if poll_thread and poll_thread.is_alive():
|
||||
logger.error("微信 ClawBot 消息轮询线程未在关闭预算内退出")
|
||||
return False
|
||||
self._poll_thread = None
|
||||
return True
|
||||
|
||||
def _start_polling(self) -> None:
|
||||
"""启动消息轮询线程。"""
|
||||
|
||||
Reference in New Issue
Block a user