mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-02 21:55:22 +08:00
fix(plugins): 审查修复——示例插件与通知渠道的若干问题
- run-command:等待进程退出而不是等 stderr 关闭(后台子进程不再导致误判超时),
stderr 写临时文件取末尾;只替换整项为 {字段} 的参数,{files} 展开为每个文件
一个参数,夹带的占位符原样保留;不把宿主自己的 AB_* 环境变量传给命令;README
说明脚本从 AB_* 环境变量读取事件数据
- strm-writer:路径规范化后为媒体根目录本身(. / ..)时跳过,不再写出 STRM 目录之外的文件
- slack-notifier / onebot-notifier:带海报发送失败时去掉图片只发文字重试一次
- wpush-notifier / onebot-notifier:非 2xx 时报告 HTTP 状态,响应不是 JSON 时不再报解析异常
- 插件通知渠道的 extra 包含已填写的已声明字段(group、icon 等),不只是 model_extra
- 新增通知渠道时切换类型会清掉上一类型的字段,避免 Bark 带上残留的 Webhook URL
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
``file.renamed`` 运行 ``command``。命令是参数列表,直接 ``exec``,不经过 shell,
|
||||
所以参数里的空格、``$``、``;`` 都按字面传给程序。
|
||||
|
||||
事件字段以 ``AB_*`` 环境变量传给命令,参数里也可以写 ``{字段}`` 占位符:
|
||||
事件字段以 ``AB_*`` 环境变量传给命令。整个参数恰好是 ``{字段}`` 时替换为字段值
|
||||
(``{files}`` 展开为每个文件一个参数);参数里夹带的占位符不替换,以免文件名被拼进
|
||||
``sh -c`` 之类的脚本文本:
|
||||
|
||||
- 两个事件都有:``AB_EVENT``、``AB_BANGUMI_ID``(旧种子可能为空)、``AB_DOWNLOADER_ID``
|
||||
- ``torrent.organized``:``AB_TORRENT_HASH``、``AB_FILES``(正片与字幕的路径,每行一个)
|
||||
@@ -17,7 +19,8 @@
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
from subprocess import DEVNULL, PIPE
|
||||
import tempfile
|
||||
from subprocess import DEVNULL
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -32,7 +35,10 @@ class Options(BaseModel):
|
||||
command: list[str] = Field(
|
||||
default_factory=list,
|
||||
title="命令",
|
||||
description="程序及其参数,每项一个,如 /config/scripts/link.sh、{files};不经过 shell",
|
||||
description=(
|
||||
"程序及其参数,每项一个,如 /config/scripts/link.sh、{files};不经过 shell,"
|
||||
"只替换整项为占位符的参数"
|
||||
),
|
||||
)
|
||||
events: list[str] = Field(
|
||||
default_factory=lambda: [TorrentOrganized.kind],
|
||||
@@ -70,13 +76,29 @@ def event_fields(event: Event) -> dict[str, str] | None:
|
||||
|
||||
|
||||
def build_argv(command: list[str], fields: dict[str, str]) -> list[str]:
|
||||
"""替换参数里的 ``{字段}``;未知的占位符原样保留。"""
|
||||
return [_PLACEHOLDER.sub(lambda m: fields.get(m[1], m[0]), arg) for arg in command]
|
||||
"""整项为 ``{字段}`` 的参数替换为字段值,``{files}`` 展开为每个文件一个参数。
|
||||
|
||||
部分包含占位符的参数与未知的占位符原样保留。
|
||||
"""
|
||||
argv: list[str] = []
|
||||
for arg in command:
|
||||
m = _PLACEHOLDER.fullmatch(arg)
|
||||
if m is None or m[1] not in fields:
|
||||
argv.append(arg)
|
||||
elif m[1] == "files":
|
||||
argv.extend(fields["files"].splitlines())
|
||||
else:
|
||||
argv.append(fields[m[1]])
|
||||
return argv
|
||||
|
||||
|
||||
def build_env(fields: dict[str, str]) -> dict[str, str]:
|
||||
"""继承宿主环境(否则子进程没有 PATH),再加上 ``AB_*``。"""
|
||||
return {**os.environ, **{f"AB_{k.upper()}": v for k, v in fields.items()}}
|
||||
"""继承宿主环境(否则子进程没有 PATH),再加上 ``AB_*``。
|
||||
|
||||
宿主自己的 ``AB_*``(如 ``AB_DOWNLOADER_PASSWORD``)不传给命令。
|
||||
"""
|
||||
env = {k: v for k, v in os.environ.items() if not k.startswith("AB_")}
|
||||
return {**env, **{f"AB_{k.upper()}": v for k, v in fields.items()}}
|
||||
|
||||
|
||||
class RunCommand(Plugin[Options]):
|
||||
@@ -91,26 +113,34 @@ class RunCommand(Plugin[Options]):
|
||||
if fields is None:
|
||||
return
|
||||
argv = build_argv(cfg.command, fields)
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*argv, stdin=DEVNULL, stdout=DEVNULL, stderr=PIPE, env=build_env(fields)
|
||||
)
|
||||
except OSError as e:
|
||||
self.ctx.log.warning("无法启动命令 %s:%s", argv[0], e)
|
||||
return
|
||||
try:
|
||||
async with asyncio.timeout(cfg.timeout):
|
||||
_, stderr = await proc.communicate()
|
||||
except TimeoutError:
|
||||
self.ctx.log.warning("命令 %s 超过 %s 秒,已终止", argv[0], cfg.timeout)
|
||||
return
|
||||
finally:
|
||||
# 超时或宿主取消处理时,不留下孤儿进程
|
||||
# ponytail: 只杀直接子进程;命令自己再派生的进程需由脚本负责
|
||||
if proc.returncode is None:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
tail = stderr[-2000:].decode(errors="replace").strip()
|
||||
# stderr 写进临时文件而不是管道:命令留在后台的子进程继承 stderr 时,
|
||||
# 读管道要等到它们退出,命令本身早已结束也会被当成超时
|
||||
with tempfile.TemporaryFile() as err:
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*argv,
|
||||
stdin=DEVNULL,
|
||||
stdout=DEVNULL,
|
||||
stderr=err,
|
||||
env=build_env(fields),
|
||||
)
|
||||
except OSError as e:
|
||||
self.ctx.log.warning("无法启动命令 %s:%s", argv[0], e)
|
||||
return
|
||||
try:
|
||||
async with asyncio.timeout(cfg.timeout):
|
||||
await proc.wait()
|
||||
except TimeoutError:
|
||||
self.ctx.log.warning("命令 %s 超过 %s 秒,已终止", argv[0], cfg.timeout)
|
||||
return
|
||||
finally:
|
||||
# 超时或宿主取消处理时,不留下孤儿进程
|
||||
# ponytail: 只杀直接子进程;命令自己再派生的进程需由脚本负责
|
||||
if proc.returncode is None:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
err.seek(max(0, err.seek(0, os.SEEK_END) - 2000))
|
||||
tail = err.read().decode(errors="replace").strip()
|
||||
if proc.returncode:
|
||||
self.ctx.log.warning(
|
||||
"命令 %s 退出码 %s:%s", argv[0], proc.returncode, tail
|
||||
|
||||
Reference in New Issue
Block a user