Rust in Everything
基于 tauri 2.12.5

state not managed for field counter on command hit

主线位置:调用主线中段,命令包装为每个参数调 CommandArg::from_command 的时候。State<'_, T> 的实现到 StateManager 里按 T 的 TypeId 查找(状态),查不到就返回错误,命令函数不会执行。另一半在启动主线上:Builder::manage 把值放进同一张表,同一类型放第二次就 panic。

flowchart TB
  put["manage 值的类型"] --> table["StateManager 按 TypeId"]
  ask["State 的类型参数"] --> look["按 TypeId 查"]
  table --> look
  look -->|"类型完全相同"| okn["拿到引用"]
  look -->|"差一层 Mutex 或 Arc"| errn["state not managed"]

本页实验 crate 是 pit1-state:命令和测试在 src/main.rs,用 tauri::test 的 MockRuntime 跑(cargo test -- --nocapture),不开窗口;另有一个 bin dup 演示 Builder::manage 两次。M2 Max、macOS 27、rustc 1.99.0,tauri 2.12.5,debug 构建。

现象

命令要一个加了锁的计数器:

#[derive(Default)]
struct Counter {
  hits: u32,
}

#[tauri::command]
fn hit(counter: State<'_, Mutex<Counter>>) -> u32 {
  let mut counter = counter.lock().unwrap();
  counter.hits += 1;
  counter.hits
}

测试分别用四种 manage 建应用,各调一次 hit(get_ipc_response,请求体 {}):

    let (_a, w) = app(mock_builder());
    println!("no manage           : {}", call(&w, "hit"));
    let (_a, w) = app(mock_builder().manage(Counter::default()));
    println!("manage(Counter)     : {}", call(&w, "hit"));
    let (_a, w) = app(mock_builder().manage(std::sync::Arc::new(Mutex::new(Counter::default()))));
    println!("manage(Arc<Mutex<Counter>>): {}", call(&w, "hit"));
    let (a, w) = app(mock_builder().manage(Mutex::new(Counter::default())));
    println!("manage(Mutex<Counter>): {} then {}", call(&w, "hit"), call(&w, "hit"));
no manage           : Err("state not managed for field `counter` on command `hit`. You must call `.manage()` before using this command")
manage(Counter)     : Err("state not managed for field `counter` on command `hit`. You must call `.manage()` before using this command")
manage(Arc<Mutex<Counter>>): Err("state not managed for field `counter` on command `hit`. You must call `.manage()` before using this command")
manage(Mutex<Counter>): Ok(1) then Ok(2)

后两种错得最隐蔽:代码里明明调了 .manage(),错误却说「You must call .manage()」。

同一个类型在 Builder 上 manage 两次(比如 main 里一次,某个帮手函数里又一次),src/bin/dup.rs:

struct ApiBase(String);

fn main() {
  let _builder = tauri::Builder::default()
    .manage(ApiBase("https://a.example".into()))
    .manage(ApiBase("https://b.example".into()));
}

thread 'main' (2792851) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.12.5/src/app.rs:2060:5:
state for type 'dup::ApiBase' is already being managed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101

这个 panic 发生在 Builder 链上,事件循环还没启动,是普通的 Rust panic(退出码 101)。

出处

State 的 CommandArg 实现 调 try_state::<T>(),取不到就构造这条消息,command.key 是参数在 JSON 里的键(counter),command.name 是命令名:

    command.message.webview_ref().try_state().ok_or_else(|| {
      InvokeError::from_anyhow(anyhow::anyhow!(
        "state not managed for field `{}` on command `{}`. You must call `.manage()` before using this command",
        command.key, command.name
      ))
    })

try_state 背后是一张以 TypeId 为键的表,T 必须和 manage 时的值类型完全相同。Counter、Mutex<Counter>、Arc<Mutex<Counter>> 是三个不同的 TypeId,没有任何自动解包。manage 的值本身已经被放进 Box 并在应用的生命周期里保持不动(状态),多包一层 Arc 没有意义,反而让类型对不上。

这个错误是 Result 的 Err,JS 侧 Promise 被拒绝,应用继续运行;2.2.0 之前它是 panic(状态 的冷知识)。不在命令里、而是直接调 Manager::state 时还是 panic,测试里用 catch_unwind 接住:

thread 'tests::state_cases' (2792372) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.12.5/src/lib.rs:761:7:
state() called before manage() for pit1_state::Counter

在同步命令里这样 panic,进程在 macOS 上会直接 abort(异步运行时 的动脑筋答案);事件循环回调里的 panic 也一样(a webview with label main already exists 里 setup 出错的那次运行)。

两次 manage 的 panic 来自 Builder::manage 里的 assert!:链式调用没有地方返回 bool。Manager::manage(app.manage(..),在 setup、插件、命令里用)同样情况返回 false,不 panic,第二个值被丢掉,表里还是第一个。

修法

manage 的类型和 State 的类型参数写成同一个。最省事的是给它起个名字,两边都用这个名字:

type CounterState = Mutex<Counter>;

// 注册
tauri::Builder::default().manage(CounterState::default())

// 使用
#[tauri::command]
fn hit(counter: State<'_, CounterState>) -> u32 { /* ... */ }

manage(Mutex<Counter>) 那一行就是这个写法的结果:Ok(1) then Ok(2),状态在两次调用之间保留。

可能被注册两次的状态(插件、可复用的初始化函数里),不要放在 Builder::manage 上,改在 setup 里 app.manage(..) 并检查返回值;只是读取时可能还没注册的地方用 try_state,测试里 try_state::<Counter>() 在只注册了 Mutex<Counter> 时返回 None(输出 try_state::<Counter>() = false)。

怎么确认没中招

  • 错误消息里只有字段名和命令名,没有类型名。拿到这条错误时,把命令签名里 State<'_, X> 的 X 和所有 .manage(..) 的值类型逐字对一遍,Mutex、RwLock、Arc 都算在类型里。
  • 每个命令至少用 MockRuntime 调一次(像本页这样 mock_builder() 加上和真实应用相同的 manage 链),状态没注册或类型不对,测试里就是一个 Err。
  • 启动时 panic「state for type '...' is already being managed」:在代码里搜这个类型的所有 .manage(。

小结

  • State<'_, T> 按 T 的 TypeId 查找,Counter、Mutex<Counter>、Arc<Mutex<Counter>> 互不相通;查不到时命令以「state not managed for field ... on command ...」被拒绝,消息不带类型名。
  • 用一个类型别名同时写 manage 和 State,就不会差一层。
  • Builder::manage 同一类型两次会在启动前 panic;Manager::manage 返回 false。
  • 命令外直接 app.state::<T>() 取不到时仍然 panic,拿不准就用 try_state。

💬 关联讨论

发起讨论
加载中...