Skip to content

fix(task): 统一脚本停止清理并修复 MaaFW 资源释放 - #1892

Merged
youngslin merged 11 commits into
AUTO-MAS-Project:devfrom
youngslin:fix/maafw-stop-finalizer-lock-20261109
Nov 9, 2026
Merged

youngslin merged 11 commits into
AUTO-MAS-Project:devfrom
youngslin:fix/maafw-stop-finalizer-lock-20261109

Conversation

@youngslin

@youngslin youngslin commented Nov 9, 2026 •

Copy link
Copy Markdown

摘要

  • 统一当前脚本任务的停止清理:复用公共任务作用域,为本轮启动或明确接管的进程及后代登记归属;短命父进程、中间进程退出后,Windows Job 仍能回收后代。仅查找已有窗口不取得结束该进程的权限。

  • 补齐绕过公共入口的辅助命令:HSR 的 SRA 版本探测、7z 列表与解包,MaaEnd 差量补丁,以及鸣潮差量补丁。同步工作线程中的命令继承调用任务归属,任务外调用保持原有行为。

  • 保留逐项限时停止确认,超时后仍强制回收任务归属进程并关闭本地管道;一个进程组失败不跳过其余清理。不等待输出 EOF 来证明退出,Windows 组退出核验最多等待 1 秒,未确认时记录失败并保留资源占用。

  • 修复旧专项收尾被遗弃引起的 MaaFW 配置锁、项目预约与结果写回问题;Windows 退出通知滞后时,用原生进程句柄独立核验并记录日志,仍等待任务清理与文件核对完成才释放预约。

  • 修复审查指出的发行包核对取消后永久占用:核对通过停止令牌退出后,等待真实任务清理成功再释放预约;若释放等待者被取消而核对仍在运行,则保留预约。

  • 修复 Sentry 指出的 Windows 接管竞态:确认旧目标已经退出后跳过它,继续登记已发现的后代;仍存活目标的权限等错误继续上报并保留资源占用。

覆盖范围

任务入口 清理范围与本轮核对结果
MAA、SRC、MaaEnd、通用脚本、BetterGI、BAAH、OK-WW、OK-NTE、绝区零一条龙、Whimbox 主脚本及已有的原生配置会话通过 ProcessManager 进入公共作用域;本轮归属的进程树参与统一停止清理。
HSR 各引擎运行入口与云游戏浏览器进程使用 ProcessManager;本轮补齐 SRA 版本探测及 M7A 更新所用的 7z 列表、解包命令。
MaaFW、M9A、MSS M9A、MSS 复用 MaaFW 引擎;worker、其启动的 agent、运行前设置与接入进程工厂的环境安装命令参与清理,项目预约等待真实收尾。
MaaEnd / 鸣潮更新 补齐 hpatchz 差量应用命令的任务归属,停止时回收仍占管道、写文件的后代。
用户前后置命令、任务级下载命令 复用现有 ProcessRunner / 任务作用域;共享下载服务、主程序自更新安装器等独立服务不改为任务级清理。

上述是代码入口和公共清理链路的核对结果,不代表每个真实游戏均完成实机验收。

验证

最新提交:ce1c959e4efe27202b9ab880e64d27cc530238fc。作者 Windows 本机、Python 3.12.13,独立环境按 uv.lock 安装。

  • 本地全量:1755 passed、2 skipped、17 subtests passed;相关更新回归 94 passed。
  • 同步命令和鸣潮补丁的原启动方式,在停止后均留下持续写心跳的后代(两项失败);接入公共入口后,同一场景确认后代退出、写入停止、输出等待结束,任务外对照进程存活。
  • 新增真实集成回归:导入最小 MaaFW 项目,持有同谱系更新锁,触发实际发行包核对并停止任务;修复前项目无法再次预约,修复后可重新预约。既有的未确认清理保留预约、后台清理完成后释放用例也通过。
  • Sentry 回归:真实子进程在 OpenProcess 前退出并由父进程回收句柄,原生调用返回错误 87;修复前登记失败,修复后继续回收存活孙进程和兄弟进程,任务外进程不受影响。对存活子进程注入权限错误时仍拒绝登记并保留失败标记。进程接管与 MaaFW 预约相关回归共 25 项通过。
  • 已有回归覆盖:接管进程的已有/后续后代及短命中间进程,单组清理失败仍尝试其他目标,登记失败保留资源占用,MAA 配置先保存关闭再强制清理,MaaFW 停止后再次预约,原生退出已发生但 asyncio 通知滞后的独立核验。
  • Windows / Linux / macOS 平台 mypy、ruff、更新日志与锁文件检查通过;上一提交的 Windows / Ubuntu 全量测试、三平台接口检查、格式及构建 CI 均已通过;本次 Windows 进程竞态修复的远端 CI 已触发,等待结果。

边界

仅处理本轮明确归属的进程,不按全局进程名结束共享 ADB 或任务外已有程序,不猜测登记前已完全失去谱系的历史进程。内部资源清理沿用已有任务结果与本地日志,不增加遥测字段或发送 PID、路径。

使用真实任务生命周期及本机标记进程验证,游戏执行和第三方更新工具由受控测试进程替代;没有使用真实游戏账号、模拟器或共享 ADB 服务。临时副本与测试目录在验证后删除,主测试目录和日常安装不变。本次不合并 PR。

jinghero commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

总体结论:修改后合并

AUTO-MAS-Project/AUTO-MAS #1892

宝宝,把“停止等待超时”和“真正清理完毕”分开,确实能避免把解锁工作一并丢掉。不过这版只保住了各层清理任务,还没保住父子收尾的先后关系:子任务仍在写账号状态时,父管理器已经可以把配置锁放开了。这里先别急着合喵,回归测试也还没接到它想验证的那条回调链。

旧执行桥负责停止确认与收尾等待;MaaFW 的账号任务完成资源关闭和账号结算,管理器随后保存配置、释放托管锁。这次需要保留的正是后两步的顺序,不需要另建一套锁机制。

[逻辑 · P2] 保留子任务清理时,也要让父管理器等到真正完成再解锁

位置:app/task/legacy.py:283–304。

静态追踪:MaaFW 管理器在 _finalize_inner_task() 中先把 _inner_finalized 设为真,再等待账号执行桥的 finalizer。停止确认超时取消这个外层 finalizer 后,新加的 shield(cleanup) 让真正的账号收尾继续运行,但父管理器已经收到取消,进入自己的 final_task();它看到 _inner_finalized 为真便跳过等待,随后 _commit_user_data() 释放配置 X 锁。对应调用在 embedded_manager.py:1889–1944。

这不是只有永不返回的协程才会遇到:账号收尾的模拟器关闭允许等待 30 秒,执行桥默认收尾确认只有 10 秒。关闭较慢、停止确认先到期时,账号清理仍可能停在 _close_emulator();随后它还会写入剩余天数、代理次数和上次状态,见 runner_task.py:1115–1153。此时配置已经允许编辑或删除,迟到的结算可能覆盖用户刚改的值,或因对象被删除而失败。

项目目录预约仍会挡住部分重跑、导入操作,但它不代替账号配置锁;retain_worker() 也只保留任务引用,不让父层等待子层完成。原实现会把取消传入实际账号收尾,这次新增的“子收尾继续、父层已解锁”窗口需要补上。

建议由旧执行桥保存真正的清理完成句柄,让父层的后台收尾继续等待子层真实完成后再保存、解锁;面向用户的停止等待仍可限时返回。请补一个真实 manager + inner task 的场景:账号清理越过停止等待期限时配置仍锁定,允许账号清理完成后才解锁,并且结算只发生一次。把这条顺序补齐,保留清理的改动才算完整啦。

[规范 · P2] 新回归测试的阻塞回调提交到了随后被替换的对象

位置:tests/core/test_maafw_stop_lock_regression.py:42–60。

测试先调用 manager.callbacks.submit(pending_write()),随后 Root.main_task() 才执行 self.spawn(manager)。但 TaskExecuteBase.spawn 会把 manager.callbacks 替换为父任务的回调桥。屏障还留在旧对象里,管理器实际排空的是另一个空队列,因此停止返回前配置已经解锁。

这点不只是推演:已读取本次 Python Tests 的实际日志,Ubuntu 和 Windows 都在第 60 行 assert config.is_locked 失败。测试目前既不能证明修复,也会阻断全量回归。

建议在 spawn() 完成回调桥绑定后,例如替换的 run_manager() 内,向管理器实际使用的桥提交屏障,再核对停止返回、回调完成、解锁和重新取锁的顺序。保留这些行为断言,别只把断言改到变绿喵。

验证与合入检查

本次完成源码静态追踪与两路独立复核,没有运行项目代码或测试。现有 CI 还报告旧测试 test_started_account_finalizer_saves_history_once_with_bounded_confirmation[True] 预期零条历史、实际一条;保留迟到收尾是本 PR 的意图,需同步改为等待真实清理后验证恰好一条,而不是把新行为直接当成生产缺陷。

另已读取格式和更新日志检查的失败日志:两个改动文件需要按仓库锁定的格式工具处理;更新日志检查未找到碎片,当前也没有豁免标签。请按项目本轮发布规则补齐碎片或适用的豁免,再让 CI 核验。构建成功不能替代上述失败的回归检查。

—— dot酱敬上 🐾

@github-actions github-actions Bot added 模块: 调度 按更新日志碎片自动设置 类型: 修复 按更新日志碎片自动设置 labels Nov 9, 2026

jinghero commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

总体结论:修改后合并

AUTO-MAS-Project/AUTO-MAS #1892

宝宝,这版已经补上重复取消的等待保护,也让停止确认期间的设备关闭和后脚本继续走原来的进程管理入口,前一版的两个方向都收得更稳了。不过新加的项目预约延迟释放还会被另一条取消路径打断:进程已经成功结束,项目却可能一直占用。这里先别合,最后这段释放链还要补齐喵。

旧执行桥负责限时确认和统一进程清理;MaaFW 账号任务持有项目预约,只有确认旧工作不再操作目录后才能释放。这个责任划分没问题,问题是等待的清理任务没有一起保留下来。

[逻辑 · P2] 成功强杀后,取消掉的清理任务会让项目预约永久遗留

位置:runner_task.py:3690–3732。

静态推演的触发条件是停止前尚未开始收尾,运行前设置进程的清理超过停止确认预算,但统一强杀成功。具体顺序:

  1. MaaFW 正在停止运行前设置进程。_terminate_pretask_process() 创建 pretask_cleanup,这个任务继承当前进程作用域,但没有调用 retain_worker()。它可能仍在等待进程后代快照线程。
  2. 停止确认到期,统一 force_close() 成功结束进程;随后 旧执行桥的统一取消 会取消没有被保留的 pretask_cleanup。
  3. 新释放逻辑把 project_lock_key 从账号对象移走,再让保留的 worker 等待 shield(cleanup)。但里面的 cleanup 已经被取消,shield 仍会抛 CancelledError;这里只捕获 Exception,释放 worker 因而退出,执行不到 release_project_path_sync(key)。
  4. 即使原快照线程随后结束,项目预约也没有新的释放入口,账号对象上的 key 已是 None。同项目再次运行、导入或更新仍会被判定为占用。

这里没有假设强杀失败,也不要求提前放开仍在写入的资源。新增的 scope.cleanup_failed 检查在成功强杀场景不会拦截;保留释放 worker 也不能让它等待的另一个任务免于取消。在上述“停止前尚未收尾”的场景,基线先强杀、再启动账号收尾,pretask_cleanup 在统一取消扫描之后才创建;本次把收尾提前到扫描之前,又新增了这段等待被取消 cleanup 的释放路径,造成新的永久遗留窗口。

建议由 MaaFW 清理入口保留能够代表真实完成的 cleanup 句柄,让它能走到结束,释放 worker 再据此确认并交还预约。不要仅捕获取消就直接释放,因为取消本身不证明底层工作已完成。可以复用新增的 test_unconfirmed_pretask_stop_keeps_project_reserved 屏障:另补不注入强杀失败的一支,让进程快照超过确认预算,确认强杀成功,再放行快照,最终必须能重新取得同一项目预约。这样既守住“未确认不放锁”,也守住“确认结束后不永远占着”啦。

复审与验证

前一条评论中“子任务后台完整收尾、父层先解锁”的原路径已经撤掉;错误回调桥夹具也已移除。新增清理命令和真实账号预约回归有用,但不等价于原来的 manager 配置 X 锁契约:仍需在真实回调桥上验证回调结清后能重新取得配置锁,不能以项目预约测试代替。当前不重复列旧缺陷;新测试也还没有覆盖上面这条成功强杀后的最终释放。

dot酱做了源码静态追踪和两路独立复核,没有执行项目代码或测试。已读取当前 Python Tests 实际日志,Windows、Ubuntu 都还剩 3 项 TimeoutError,分别涉及晚到进程句柄、文件线程和 MaaFW 环境准备。它们不是本条静态缺陷的实际复现;停止等待预算和夹具时序仍需核对,不能直接把所有超时当成同一个原因。当前格式、更新日志、锁文件、误回退和构建检查均已通过,合入前仍需让行为回归通过。

—— dot酱敬上 🐾

@youngslin youngslin changed the title fix(task): 停止超时后仍完成 MaaFW 等旧专项的资源解锁 fix(task): 修复停止后的进程清理与 MaaFW 资源释放 Nov 9, 2026
@youngslin youngslin changed the title fix(task): 修复停止后的进程清理与 MaaFW 资源释放 fix(task): 统一脚本停止清理并修复 MaaFW 资源释放 Nov 9, 2026

jinghero commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

总体结论:修改后合并

AUTO-MAS-Project/AUTO-MAS #1892

宝宝,上一轮的 pretask 清理任务这次确实保留下来了,还补了强制停止成功、慢快照结束后再释放预约的回归,这处修得好喵~不过发行包核对还有另一条正常取消路径,会让已经停下来的项目一直显示占用;这只漏网 bug 还得一起补上。

公共任务作用域负责停止确认和进程回收,MaaFW 账号负责等文件核对真正结束后释放项目预约。现在卡住的是这两个步骤之间的交接。

[逻辑 · P2] 发行包核对正常取消后,项目预约不会释放

位置:runner_task.py:3723–3728。

静态追踪到的触发过程是:资源加载失败进入 _recover_resource_failure(),发行包核对正在等待同一项目的更新锁,此时用户点停止。停止令牌会使等锁退出,UpdateSession.run() 把这个正常停止转换成 CancelledError,因此 _release_check_task 已结束但处于 cancelled 状态。

账号收尾把预约键移交给延后释放任务,并把自身的 project_lock_key 清为 None。即使进程清理成功,await asyncio.shield(pending) 仍会立即抛出 CancelledError;它不属于 Exception,这里接不住,后面的 release_project_path_sync(key) 就不会执行。保留任务句柄也挡不住任务自身的正常取消,scope.cleanup_failed 在这个场景下仍是 false。

结果是项目预约一直留在进程内的占用表中,没有过期释放;同一项目之后再运行、重新导入或手动更新都会被占用检查拒绝,直到重启后端。原实现对已结束的核对任务会直接释放预约,这条卡住路径是本 PR 新增的。

建议让 MaaFW 的释放逻辑明确识别“核对已经安全结束,包括协作取消”,确认后继续释放;尚未完成的文件操作仍要保留预约,别简单吞掉所有取消就放锁喵。可以沿现有预约测试补一个针对性场景:占住同源更新锁,让真实发行包核对进入等锁,再停止任务,确认清理成功后能重新取得同一项目预约。

本轮复查了停止生命周期、Windows 进程归属、同步更新命令及 MaaFW 收尾链路。已读取最新 Windows / Ubuntu CI 日志,两平台均通过;现有预约回归覆盖模拟器关闭和 pretask 清理,还没有覆盖上述发行包核对取消路径。dot酱只做了静态检查,没有自行运行代码、测试或实机验证。把这条释放分支补齐,再用对应回归确认就好啦。

—— dot酱拍拍爪敬上 🐾

jinghero commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

总体结论:可以合并

AUTO-MAS-Project/AUTO-MAS #1892

宝宝,这次把正常停止后的预约释放补齐了喵:发行包核对已经协作取消并结束时,会继续归还项目预约;只是释放等待者被取消、核对还在运行时,仍不会提前放锁。前面进程清理失败时保留占用的保护也没有放宽,这处分清了,dot酱给你拍拍爪~

新增回归走真实的资源失败恢复和更新锁等待,再停止任务并确认同一项目可以重新预约,正对上一条问题。本轮复查未发现还需修改的确认缺陷,审查范围内可以合并。

已读取本次 Windows / Ubuntu CI 实际日志,两平台通过。dot酱复用了未变部分的既有审查,针对新取消分支和整体停止链路做静态复核,没有自行运行项目代码,也没有实机验证;PR 目前仍是 draft,是否转为就绪由维护者决定。

—— dot酱拍拍爪敬上 🐾

@youngslin
youngslin marked this pull request as ready for review November 9, 2026 19:29
Comment thread app/utils/windows/process.py Outdated
Comment on lines +82 to +91
process = win32api.OpenProcess(
win32con.PROCESS_SET_QUOTA
| win32con.PROCESS_TERMINATE
| win32con.PROCESS_QUERY_INFORMATION,
False,
target.pid,
)
try:
# OpenProcess 的句柄固定目标;再用缓存的创建时间核对身份。
if not target.is_running():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: WindowsTaskJob.adopt() fails with an unhandled pywintypes.error if a descendant process exits before OpenProcess, causing cleanup_failed to stay set and project locks to leak.
Severity: MEDIUM

Suggested Fix

Wrap win32api.OpenProcess inside WindowsTaskJob.adopt() in a try...except pywintypes.error block to catch errors caused by terminated processes and skip or ignore processes that are no longer accessible.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: app/utils/windows/process.py#L82-L91

Potential issue: In `WindowsTaskJob.adopt()`, `win32api.OpenProcess` is called on the
root process and all queued descendants. If a short-lived child process (such as a
launcher helper or crash reporter) exits between being listed and opened, `OpenProcess`
raises a `pywintypes.error` that is not caught or tolerated. This unhandled exception
aborts process adoption and causes `scope.track()` to set `cleanup_failed = True`.
Consequently, `release_after_confirmed_stop` skips releasing the project lock, leaving
the MaaFW project reserved until the application is restarted.

Did we get this right? 👍 / 👎 to inform future reviews.

@youngslin
youngslin merged commit cec8658 into AUTO-MAS-Project:dev Nov 9, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

模块: 调度 按更新日志碎片自动设置 类型: 修复 按更新日志碎片自动设置

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants