由 Sentry 每日分诊自动建立。Sentry issue: AUTO-MAS-BACKEND-5T / AUTO-MAS-BACKEND-5V / AUTO-MAS-BACKEND-5W
https://auto-mas.sentry.io/issues/AUTO-MAS-BACKEND-5T
https://auto-mas.sentry.io/issues/AUTO-MAS-BACKEND-5V
https://auto-mas.sentry.io/issues/AUTO-MAS-BACKEND-5W
现象
通用脚本在恢复「脚本直控配置」前,会先把用户填的 ConfigPath 整个目录删掉再从快照拷回。删除这一步用的是裸 shutil.rmtree,Windows 上目录里有任一文件被占用就抛 PermissionError: [WinError 32],而 rmtree 是边遍历边删的,抛出时目录已经被删掉一部分,后面的拷回永远走不到 。
4 条事件,同一 trace(c351e2b0f8d049039b5f22c1757b7307)、同一用户(CN 南阳)、同一秒,v5.5.0-beta.2(dist 66fdb0c7ba5d)、trigger: manual_task、mode: AutoProxy。
该用户的 ConfigPath 是 F:\qq\MAA-v5.9.0-win-x64,即整个 MAA 安装目录。
三条 Sentry issue 是同一次故障 沿三条调用路径各报一次,见下。
定位
行号为最新 dev(978c8b30),与线上 v5.5.0-beta.2 事件里的行号逐行一致,未修复。
app/task/general/manager.py:139 _remove_script_config:
def _remove_script_config (self ) -> None :
"""清理脚本当前配置路径,避免不同来源的目录文件互相残留。"""
if self .script_config_path .is_dir ():
shutil .rmtree (self .script_config_path ) # <- :142,无 ignore_errors / 无重试
elif self .script_config_path .exists ():
self .script_config_path .unlink ()
app/task/general/manager.py:162 _restore_external_config:先 _remove_script_config()(:167),再 copytree(temp -> script_config_path)(:173)。删除抛异常 → 拷回那行不会执行 。
三个调用点:
调用点
行
事件
main_task 换用户前恢复直控配置
:270
5V
on_crash 兜底恢复
:376
5V / 5W
final_task(asyncio.shield)收尾恢复
:302
5T / 5W
根因
删除无容错 ::142 是裸 rmtree。同一仓库里 OK-NTE 的对应逻辑(app/task/OkNte/manager.py:194/200/202/216)四处 rmtree 全部带 ignore_errors=True,通用脚本这一处是唯一的例外。dev 上目前没有任何 force_rmtree 之类的重试封装(git grep force_rmtree 在 dev 的 app/ 下无命中;引入它的 PR fix(oknte): 修复脚本配置目录含只读文件时备份与复原失败 #456 只改 OK-NTE,仍开着)。
先删后拷、失败不回滚 :_restore_external_config 的删除与拷回不是原子的,删除中途失败就停在「用户目录被删了一半、快照还在 temp 里没拷回来」的状态。快照本身是完好的(external_config_snapshot_ready 仍为 True,temp 内容还在),但代码没有任何路径能跳过删除直接拷回。
失败后连删三次 :main_task:270 抛出后 → models/task.py:276 调 on_crash → on_crash:376 再调一次 _restore_external_config() → 同一个锁、同一行再抛;随后 final_task:302 第三次。占用只要还在,三次都只会继续删、不会恢复。事件里 on_crash 自己也带着 PermissionError 退出,说明崩溃处理链本身被打断了。
不确定的部分(未验证 ):占用 F:\qq\MAA-v5.9.0-win-x64 的是哪个进程没有直接证据。main_task:270 的恢复发生在运行下一个用户之前 ,最可能是上一轮的脚本进程句柄尚未释放。
复现与影响
复现:通用脚本配置两个用户、其中至少一个用「脚本直控配置」,ConfigPath 指向一个仍被脚本进程占用的目录(例如脚本自身的安装目录),跑自动代理。
影响:ConfigPath 指向的目录被部分删除且不会恢复。本次事件里该路径是用户的整个 MAA 安装目录。
相关
顺带记录(未由本次事件触发,仅为读代码所见)
GeneralConfig.Script_ConfigPath 默认值为空串且 FileValidator 放行空串(app/models/config.py:3274),而通用脚本的 check()(manager.py:69)不校验 ConfigPath。若 ConfigPath 为空,Path("") 等价于 Path(".")、is_dir() 为 True,_remove_script_config 会对进程 cwd(main.py 里 chdir 到的安装根)调 rmtree。这条路径没有在 Sentry 里观察到 ,未验证是否有其它前置拦截,与 #556 属同一类空路径陷阱。
现象
通用脚本在恢复「脚本直控配置」前,会先把用户填的
ConfigPath整个目录删掉再从快照拷回。删除这一步用的是裸shutil.rmtree,Windows 上目录里有任一文件被占用就抛PermissionError: [WinError 32],而rmtree是边遍历边删的,抛出时目录已经被删掉一部分,后面的拷回永远走不到。c351e2b0f8d049039b5f22c1757b7307)、同一用户(CN 南阳)、同一秒,v5.5.0-beta.2(dist66fdb0c7ba5d)、trigger: manual_task、mode: AutoProxy。ConfigPath是F:\qq\MAA-v5.9.0-win-x64,即整个 MAA 安装目录。定位
行号为最新 dev(
978c8b30),与线上v5.5.0-beta.2事件里的行号逐行一致,未修复。app/task/general/manager.py:139_remove_script_config:app/task/general/manager.py:162_restore_external_config:先_remove_script_config()(:167),再copytree(temp -> script_config_path)(:173)。删除抛异常 → 拷回那行不会执行。三个调用点:
main_task换用户前恢复直控配置:2705Von_crash兜底恢复:3765V/5Wfinal_task(asyncio.shield)收尾恢复:3025T/5W根因
:142是裸rmtree。同一仓库里 OK-NTE 的对应逻辑(app/task/OkNte/manager.py:194/200/202/216)四处rmtree全部带ignore_errors=True,通用脚本这一处是唯一的例外。dev 上目前没有任何force_rmtree之类的重试封装(git grep force_rmtree在 dev 的app/下无命中;引入它的 PR fix(oknte): 修复脚本配置目录含只读文件时备份与复原失败 #456 只改 OK-NTE,仍开着)。_restore_external_config的删除与拷回不是原子的,删除中途失败就停在「用户目录被删了一半、快照还在 temp 里没拷回来」的状态。快照本身是完好的(external_config_snapshot_ready仍为 True,temp 内容还在),但代码没有任何路径能跳过删除直接拷回。main_task:270抛出后 →models/task.py:276调on_crash→on_crash:376再调一次_restore_external_config()→ 同一个锁、同一行再抛;随后final_task:302第三次。占用只要还在,三次都只会继续删、不会恢复。事件里on_crash自己也带着PermissionError退出,说明崩溃处理链本身被打断了。不确定的部分(未验证):占用
F:\qq\MAA-v5.9.0-win-x64的是哪个进程没有直接证据。main_task:270的恢复发生在运行下一个用户之前,最可能是上一轮的脚本进程句柄尚未释放。复现与影响
ConfigPath指向一个仍被脚本进程占用的目录(例如脚本自身的安装目录),跑自动代理。ConfigPath指向的目录被部分删除且不会恢复。本次事件里该路径是用户的整个 MAA 安装目录。相关
rmtree(ignore_errors=True)掩盖占用后rename抛 WinError 5,文件与函数都不同。app/task/general/的配置拷贝,但覆盖的是ScriptConfig.py/AutoProxy.py里按ConfigPathMode拷贝的四处存在性判断,表格里没有manager.py这一层,不含本条。顺带记录(未由本次事件触发,仅为读代码所见)
GeneralConfig.Script_ConfigPath默认值为空串且FileValidator放行空串(app/models/config.py:3274),而通用脚本的check()(manager.py:69)不校验ConfigPath。若ConfigPath为空,Path("")等价于Path(".")、is_dir()为 True,_remove_script_config会对进程 cwd(main.py里 chdir 到的安装根)调rmtree。这条路径没有在 Sentry 里观察到,未验证是否有其它前置拦截,与 #556 属同一类空路径陷阱。