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。