Skip to content

fix: Guard MaaCore runtime lifecycle state#549

Merged
wangl-cc merged 4 commits into
mainfrom
codex/maa-core-runtime-lifecycle
May 24, 2026
Merged

fix: Guard MaaCore runtime lifecycle state#549
wangl-cc merged 4 commits into
mainfrom
codex/maa-core-runtime-lifecycle

Conversation

@wangl-cc

@wangl-cc wangl-cc commented May 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Move MaaCore runtime lifecycle coordination into a dedicated maa-core lifecycle module with separate runtime-loaded and linked implementations.
  • Track runtime state in one atomic word: a LOCKED bit for load/unload, a LOADED bit for runtime availability, and low bits for active assistant count.
  • Reject Assistant::load()/Assistant::unload() while assistants or runtime changes are active, and reject Assistant::new() when the runtime is unloaded or changing.
  • Use small RAII tokens only to release the runtime lock or roll back a pending assistant count reservation on construction failure.
  • Add regression tests for active assistants, runtime changes, unloaded runtime creation, failed-load lock release, count preservation while locked, and non-runtime builds.

Why

Assistant stores a raw MaaCore handle. Replacing or unloading the runtime-loaded MaaCore library while a handle is alive can leave later FFI calls, including Drop, pointing at unloaded code. The safe wrapper now reserves active assistant handles before creating them and blocks runtime replacement or unload while those reservations are alive.

The lock bit does not overwrite the loaded/count state, so existing assistants can still drop and decrement the active count while a runtime change is being rejected or unwound.

Validation

  • cargo +nightly fmt
  • cargo test -p maa-core
  • cargo check -p maa-core --no-default-features
  • cargo clippy
  • cargo x test

Summary by Sourcery

通过集中管理生命周期跟踪并强制执行安全不变式,保护 MaaCore 运行时时期在并发加载、卸载和助手创建过程中的生命周期安全。

Bug 修复:

  • 防止在有 Assistant 实例存活或正在创建时卸载或重新加载 MaaCore 运行时,从而避免对已卸载 FFI 句柄的使用(use-after-unload)。

增强功能:

  • 引入专用的运行时生命周期模块,使用原子状态跟踪运行时加载状态和活跃助手数量。
  • 通过轻量级 RAII 注册令牌将 Assistant 的创建与运行时加载状态绑定,确保在失败时正确回滚计数。
  • 在保持相同 Assistant 接口的前提下,提供分离的“运行时已加载版”和“静态链接版” MaaCore 生命周期 API 实现。

测试:

  • 新增单元测试,覆盖:活跃助手阻止运行时变更、在运行时状态迁移或未加载状态下的请求拒绝、失败时锁状态保持,以及非运行时构建场景。
Original summary in English

Summary by Sourcery

Guard MaaCore runtime lifecycle against concurrent loads, unloads, and assistant creation by centralizing lifecycle tracking and enforcing safety invariants.

Bug Fixes:

  • Prevent unloading or reloading the MaaCore runtime while Assistant instances are alive or being created, avoiding use-after-unload of FFI handles.

Enhancements:

  • Introduce a dedicated runtime lifecycle module with atomic state tracking for runtime load status and active assistant count.
  • Gate Assistant creation on runtime load state using lightweight RAII registration tokens that ensure counts are rolled back on failure.
  • Provide separate runtime-loaded and linked implementations of the MaaCore lifecycle API while preserving the same Assistant interface.

Tests:

  • Add unit tests covering active-assistant blocking of runtime changes, rejection during runtime transitions or unloaded state, lock-state preservation on failures, and non-runtime builds.

@codecov

codecov Bot commented May 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.62264% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.37%. Comparing base (5d0638c) to head (ed78d12).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
crates/maa-core/src/runtime_lifecycle/runtime.rs 93.78% 5 Missing and 7 partials ⚠️
crates/maa-core/src/lib.rs 0.00% 5 Missing ⚠️
crates/maa-core/src/runtime_lifecycle/linked.rs 64.28% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #549      +/-   ##
==========================================
+ Coverage   71.86%   72.37%   +0.50%     
==========================================
  Files          69       72       +3     
  Lines        6412     6620     +208     
  Branches     6412     6620     +208     
==========================================
+ Hits         4608     4791     +183     
- Misses       1477     1491      +14     
- Partials      327      338      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@wangl-cc wangl-cc changed the title [codex] Guard MaaCore runtime unload with active assistants fix: Guard MaaCore runtime unload with active assistants May 24, 2026
@wangl-cc wangl-cc changed the title fix: Guard MaaCore runtime unload with active assistants fix: Guard MaaCore runtime reload/unload with active assistants May 24, 2026
@wangl-cc
wangl-cc marked this pull request as ready for review May 24, 2026 20:25
@wangl-cc
wangl-cc requested a review from Copilot May 24, 2026 20:25

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - 我发现了 1 个问题,并留下了一些总体性的反馈:

  • Assistant::load/unload 中,你在 maa_sys::binding::load/unload FFI 调用之前/之后显式地释放了生命周期互斥锁,这会导致在运行时处于加载/卸载过程中的同时可以并发创建新的 assistant;建议在整个操作期间都持有该锁(或者在文档中说明这种竞争条件为何是安全的),从而将运行时状态变更与 assistant 的创建/销毁串行化。
  • Assistant::newnew_with_callbackDrop 中,生命周期互斥锁会在 FFI 调用期间一直保持,这在这些 FFI 调用如果同步触发回调且这些回调访问 Assistant::load/unload 时,可能导致死锁或异常行为;将加锁范围缩小到仅包裹计数器修改,或改用原子计数器,可以避免这种耦合。
面向 AI 智能体的提示词
Please address the comments from this code review:

## Overall Comments
- In `Assistant::load`/`unload`, you explicitly drop the lifecycle mutex before/after the `maa_sys::binding::load`/`unload` FFI calls, which allows new assistants to be created concurrently while the runtime is mid-load/unload; consider holding the lock for the entire operation (or documenting why that race is safe) so that runtime mutation and assistant creation/destruction are serialized.
- The lifecycle mutex is held across FFI calls in `Assistant::new`, `new_with_callback`, and `Drop`, which could deadlock or misbehave if those FFI calls ever invoke callbacks synchronously that touch `Assistant::load`/`unload`; minimizing the lock scope around just the counter mutation or switching to an atomic counter would avoid this coupling.

## Individual Comments

### Comment 1
<location path="crates/maa-core/src/lib.rs" line_range="96-99" />
<code_context>
+    /// Fails if any assistant instances are alive.
     pub fn load(path: impl AsRef<std::path::Path>) -> Result<()> {
         let path = path.as_ref();
+        let lifecycle = RUNTIME_LIFECYCLE
+            .lock()
+            .expect("Failed to lock MaaCore runtime lifecycle");
+        lifecycle.ensure_no_active_assistants()?;

         #[cfg(target_os = "windows")]
</code_context>
<issue_to_address>
**issue (bug_risk):** Uses of `RUNTIME_LIFECYCLE` and `Error::ActiveAssistants` need cfg-gating to compile when the `runtime` feature is disabled.

Both `RUNTIME_LIFECYCLE` and `Error::ActiveAssistants` are currently `#[cfg(feature = "runtime")]`, so these references in `load()`/`unload()` will not compile when `runtime` is disabled.

Options:
- Make `RUNTIME_LIFECYCLE` and `Error::ActiveAssistants` available unconditionally, or
- Guard the `load`/`unload` logic that uses them with `#[cfg(feature = "runtime")]` and provide a fallback for non-runtime builds.

As written, the crate fails to build without the `runtime` feature enabled.
</issue_to_address>

Sourcery 对开源项目免费 —— 如果你觉得这些代码审查有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据这些反馈改进后续的审查建议。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • In Assistant::load/unload, you explicitly drop the lifecycle mutex before/after the maa_sys::binding::load/unload FFI calls, which allows new assistants to be created concurrently while the runtime is mid-load/unload; consider holding the lock for the entire operation (or documenting why that race is safe) so that runtime mutation and assistant creation/destruction are serialized.
  • The lifecycle mutex is held across FFI calls in Assistant::new, new_with_callback, and Drop, which could deadlock or misbehave if those FFI calls ever invoke callbacks synchronously that touch Assistant::load/unload; minimizing the lock scope around just the counter mutation or switching to an atomic counter would avoid this coupling.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Assistant::load`/`unload`, you explicitly drop the lifecycle mutex before/after the `maa_sys::binding::load`/`unload` FFI calls, which allows new assistants to be created concurrently while the runtime is mid-load/unload; consider holding the lock for the entire operation (or documenting why that race is safe) so that runtime mutation and assistant creation/destruction are serialized.
- The lifecycle mutex is held across FFI calls in `Assistant::new`, `new_with_callback`, and `Drop`, which could deadlock or misbehave if those FFI calls ever invoke callbacks synchronously that touch `Assistant::load`/`unload`; minimizing the lock scope around just the counter mutation or switching to an atomic counter would avoid this coupling.

## Individual Comments

### Comment 1
<location path="crates/maa-core/src/lib.rs" line_range="96-99" />
<code_context>
+    /// Fails if any assistant instances are alive.
     pub fn load(path: impl AsRef<std::path::Path>) -> Result<()> {
         let path = path.as_ref();
+        let lifecycle = RUNTIME_LIFECYCLE
+            .lock()
+            .expect("Failed to lock MaaCore runtime lifecycle");
+        lifecycle.ensure_no_active_assistants()?;

         #[cfg(target_os = "windows")]
</code_context>
<issue_to_address>
**issue (bug_risk):** Uses of `RUNTIME_LIFECYCLE` and `Error::ActiveAssistants` need cfg-gating to compile when the `runtime` feature is disabled.

Both `RUNTIME_LIFECYCLE` and `Error::ActiveAssistants` are currently `#[cfg(feature = "runtime")]`, so these references in `load()`/`unload()` will not compile when `runtime` is disabled.

Options:
- Make `RUNTIME_LIFECYCLE` and `Error::ActiveAssistants` available unconditionally, or
- Guard the `load`/`unload` logic that uses them with `#[cfg(feature = "runtime")]` and provide a fallback for non-runtime builds.

As written, the crate fails to build without the `runtime` feature enabled.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread crates/maa-core/src/lib.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the maa-core runtime-loading wrapper by tracking live Assistant instances and preventing MaaCore runtime load()/unload() while any assistants are still alive, avoiding use-after-unload across FFI calls (including Drop).

Changes:

  • Add a global runtime lifecycle tracker (active_assistants) gated behind the runtime feature.
  • Reject Assistant::load() and Assistant::unload() when there are live assistants.
  • Add a unit test verifying load/unload are rejected while an assistant is considered active.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
crates/maa-core/src/lib.rs Introduces lifecycle tracking, increments/decrements on Assistant create/drop, and guards runtime load/unload; adds regression test.
crates/maa-core/src/error.rs Adds a new runtime-only error variant for “active assistants” lifecycle violations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/maa-core/src/lib.rs Outdated
Comment on lines 223 to 228
#[cfg(feature = "runtime")]
let mut lifecycle = RUNTIME_LIFECYCLE
.lock()
.expect("Failed to lock MaaCore runtime lifecycle");

let handle = unsafe { maa_sys::binding::AsstCreate() };
Comment thread crates/maa-core/src/error.rs Outdated
#[error("The content returned by MaaCore is too large (length > {0})")]
ContentTooLarge(usize),
#[cfg(feature = "runtime")]
#[error("Cannot change MaaCore while Assistant instances are alive")]
@wangl-cc wangl-cc changed the title fix: Guard MaaCore runtime reload/unload with active assistants [codex] Guard MaaCore runtime lifecycle state May 24, 2026
@wangl-cc
wangl-cc force-pushed the codex/maa-core-runtime-lifecycle branch from 926becd to 9b6b052 Compare May 24, 2026 22:16
@wangl-cc wangl-cc changed the title [codex] Guard MaaCore runtime lifecycle state fix: Guard MaaCore runtime lifecycle state May 24, 2026
@wangl-cc
wangl-cc requested a review from Copilot May 24, 2026 22:18
@wangl-cc

Copy link
Copy Markdown
Member Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - 我在这里先给出一些整体性的反馈:

  • PendingAssistantRegistration::begin 中,当达到 assistant 数量上限时,目前通过 expect("Assistant count overflowed") 触发 panic;建议改为返回一个专门的错误,这样在极端但仍然有效的高负载情况下,调用方就不会在 Assistant::new* 中遇到意料之外的 panic。
  • unregister_assistant 直接调用 fetch_sub(1),只依赖 debug_assert! 做防护;可能更安全的做法是在自减前显式检查 assistant 计数是否为 0(并进行日志记录/中止),这样即使 API 被误用,也不会在 release 构建中因为下溢而悄悄破坏运行时状态。
  • 在你维护了独立的 RUNTIME_STATE 的同时,Assistant::loaded() 仍然转调到 maa_sys::binding::loaded();建议考虑使用该原子状态(或者至少在文档中说明它们之间的关系),以便调用方在使用新的锁定/状态切换语义时能获得一致的视图。
面向 AI 代理的提示
请根据以下代码审查意见进行修改:

## 整体意见
-`PendingAssistantRegistration::begin` 中,当达到 assistant 数量上限时,目前通过 `expect("Assistant count overflowed")` 触发 panic;建议改为返回一个专门的错误,这样在极端但仍然有效的高负载情况下,调用方就不会在 `Assistant::new*` 中遇到意料之外的 panic。
- `unregister_assistant` 直接调用 `fetch_sub(1)`,只依赖 `debug_assert!` 做防护;可能更安全的做法是在自减前显式检查 assistant 计数是否为 0(并进行日志记录/中止),这样即使 API 被误用,也不会在 release 构建中因为下溢而悄悄破坏运行时状态。
- 在你维护了独立的 `RUNTIME_STATE` 的同时,`Assistant::loaded()` 仍然转调到 `maa_sys::binding::loaded()`;建议考虑使用该原子状态(或者至少在文档中说明它们之间的关系),以便调用方在使用新的锁定/状态切换语义时能获得一致的视图。

Sourcery 对开源项目免费——如果你觉得我们的代码审查有帮助,欢迎分享 ✨
帮我变得更有用!请对每条评论点击 👍 或 👎,我会根据你的反馈改进后续的审查。
Original comment in English

Hey - I've left some high level feedback:

  • In PendingAssistantRegistration::begin, hitting the assistant-count limit currently panics via expect("Assistant count overflowed"); consider returning a dedicated error instead so callers don’t see an unexpected panic from Assistant::new* under extreme but valid load.
  • unregister_assistant blindly does fetch_sub(1) and only guards with debug_assert!; it may be safer to explicitly check for a zero assistant count (and log/abort) before decrementing so underflow can’t silently corrupt the runtime state in release builds if the API is misused.
  • Assistant::loaded() still delegates to maa_sys::binding::loaded() while you maintain a separate RUNTIME_STATE; consider using the atomic state (or at least documenting the relationship) so callers get a view that’s consistent with the new locking/transition semantics.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `PendingAssistantRegistration::begin`, hitting the assistant-count limit currently panics via `expect("Assistant count overflowed")`; consider returning a dedicated error instead so callers don’t see an unexpected panic from `Assistant::new*` under extreme but valid load.
- `unregister_assistant` blindly does `fetch_sub(1)` and only guards with `debug_assert!`; it may be safer to explicitly check for a zero assistant count (and log/abort) before decrementing so underflow can’t silently corrupt the runtime state in release builds if the API is misused.
- `Assistant::loaded()` still delegates to `maa_sys::binding::loaded()` while you maintain a separate `RUNTIME_STATE`; consider using the atomic state (or at least documenting the relationship) so callers get a view that’s consistent with the new locking/transition semantics.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@wangl-cc
wangl-cc force-pushed the codex/maa-core-runtime-lifecycle branch from 9b6b052 to b7739c6 Compare May 24, 2026 22:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Comment thread crates/maa-core/src/error.rs Outdated
Comment thread crates/maa-core/src/runtime_lifecycle/runtime.rs Outdated
Comment thread crates/maa-core/src/runtime_lifecycle/runtime.rs Outdated
Comment thread crates/maa-core/src/runtime_lifecycle/runtime.rs
wangl-cc and others added 3 commits May 24, 2026 23:25
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@wangl-cc
wangl-cc merged commit dd4a1ba into main May 24, 2026
18 checks passed
@wangl-cc
wangl-cc deleted the codex/maa-core-runtime-lifecycle branch May 24, 2026 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants