Part of #147 (epic: DNS mode enable state desync)
背景
epic #147 的 Fix A+B(#148) 根治了 orphan proxy desync。但用户没法在 enable 卡住时主动取消——osascript 弹窗是系统级的,Tauri IPC 在 await spawn_blocking 里等 osascript 退出,前端没法打断。
范围
跨前后端的 IPC cancel 改造:
src/lib/tauri.ts::setDnsMode — 加 AbortSignal 参数透传
src/stores/profiles/actions.ts::toggleDnsModeAtom — 内部建 AbortController,catch 路径支持 cancel + 自动 revert UI
src/pages/Settings.tsx — isDnsLoading 时按钮文案变 "Cancel"(或新增显式 Cancel 按钮),点击 → abort IPC + 走 disable rollback
src-tauri/src/commands/dns.rs::set_dns_mode — 接受 app: AppHandle + 用 tokio::select! 包 enable/disable 关键路径;abort 时走 disable rollback
src-tauri/crates/mhost-dns/src/platform.rs — disable_dns_mode 已经支持 interactive=true 弹 sudo 框,直接复用
设计要点
前端
export async function setDnsMode(
enabled: boolean,
options?: { signal?: AbortSignal },
): Promise<void> {
return invoke("set_dns_mode", { enabled }, { signal: options?.signal });
}
toggleDnsModeAtom:
const ctrl = new AbortController();
abortControllerRef.current = ctrl;
try {
await setDnsMode(enabled, { signal: ctrl.signal });
...
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
// 用户主动 cancel,UI 状态保持/回退到上次成功值
// 不显示错误
} else {
...
}
}
Settings:
{isDnsLoading ? (
<button className="btn btn-danger" onClick={cancelEnable}>
Cancel
</button>
) : dnsEnabled ? (
<button onClick={() => handleToggleDns(false)}>Disable DNS Mode</button>
) : (
<button onClick={() => handleToggleDns(true)}>Enable DNS Mode</button>
)}
后端
#[tauri::command]
pub async fn set_dns_mode(
enabled: bool,
state: State<'_, AppState>,
app: AppHandle,
) -> Result<(), MhostError> {
let _guard = state.dns_lock.lock().await;
// 关键路径包 cancel token
let cancel = CancellationToken::new();
state.dns_cancel_token.lock().replace(cancel.clone());
let work = async {
if enabled {
set_dns_mode_enable(&state).await
} else {
set_dns_mode_disable(&state, true).await
}
};
tokio::select! {
result = work => result,
_ = cancel.cancelled() => {
// 用户 cancel → 走 rollback
// 启用中 cancel: 把 enable 已做的副作用撤掉
// - 如果 enable_dns_mode 已经过了 enable_dns_mode(osascript) 那一步
// → 调 set_dns_mode_disable(interactive=true) 恢复
// - 如果还没过 → server 还没启,直接 return Err(Cancelled)
// 禁用中 cancel: 大部分 disable 是 sync 的(写 signal → 等 5s),
// cancel 时直接停掉等待,proxy self-cleanup 走异步
Err(MhostError::Cancelled)
}
}
}
需要给 AppState 加 dns_cancel_token: Mutex<Option<CancellationToken>>。
难点:enable 路径是 await spawn_blocking 包 osascript,tokio::select! 不会取消 spawn_blocking,osascript 还在跑 → cancel 后必须等 osascript 自然退出再走 disable rollback(否则 networksetup 会被搞乱)。这要求 disable 路径在 cancel 时不立即执行,而是 spawn 一个 background task 等 osascript 完成后再 rollback。
实际更稳:让 cancel 走 disable_dns_mode 路径(interactive=true 弹 sudo,proxy 自己恢复)。osascript 当前可能在 & networksetup,proxy 已经被脚本里的 trap 杀掉(如果 #148 修了)。所以 cancel + #148 联动 = 完美的体验。
Acceptance criteria
不做
- 后端 IPC 层超时(留给操作系统级 cancel)
- 改动 osascript 本身(走系统 AppleScript 已经够了)
依赖
测试覆盖
- 后端
commands::dns::tests:
test_set_dns_mode_abort_before_enable_starts: cancel token pre-fired → set_dns_mode 立即返回 MhostError::Cancelled,无副作用(server 未启、proxy 未起)
test_set_dns_mode_abort_during_enable_rolls_back: mock enable_dns_mode 慢任务,cancel 后走 disable_dns_mode 把 in-memory state 清回 false
- 前端
actions.test.ts:
toggleDnsModeAtom 在 abort 时 不设 dnsErrorAtom、不 throw
- abort 后 dnsEnabled 保持旧值
Part of #147 (epic: DNS mode enable state desync)
背景
epic #147 的 Fix A+B(#148) 根治了 orphan proxy desync。但用户没法在 enable 卡住时主动取消——
osascript弹窗是系统级的,Tauri IPC 在await spawn_blocking里等 osascript 退出,前端没法打断。范围
跨前后端的 IPC cancel 改造:
src/lib/tauri.ts::setDnsMode— 加AbortSignal参数透传src/stores/profiles/actions.ts::toggleDnsModeAtom— 内部建 AbortController,catch 路径支持 cancel + 自动 revert UIsrc/pages/Settings.tsx—isDnsLoading时按钮文案变 "Cancel"(或新增显式 Cancel 按钮),点击 → abort IPC + 走 disable rollbacksrc-tauri/src/commands/dns.rs::set_dns_mode— 接受app: AppHandle+ 用tokio::select!包 enable/disable 关键路径;abort 时走 disable rollbacksrc-tauri/crates/mhost-dns/src/platform.rs—disable_dns_mode已经支持interactive=true弹 sudo 框,直接复用设计要点
前端
toggleDnsModeAtom:Settings:
后端
需要给
AppState加dns_cancel_token: Mutex<Option<CancellationToken>>。难点:enable 路径是
await spawn_blocking包 osascript,tokio::select!不会取消 spawn_blocking,osascript 还在跑 → cancel 后必须等 osascript 自然退出再走 disable rollback(否则 networksetup 会被搞乱)。这要求 disable 路径在 cancel 时不立即执行,而是 spawn 一个 background task 等 osascript 完成后再 rollback。实际更稳:让 cancel 走
disable_dns_mode路径(interactive=true弹 sudo,proxy 自己恢复)。osascript 当前可能在& networksetup,proxy 已经被脚本里的 trap 杀掉(如果 #148 修了)。所以 cancel + #148 联动 = 完美的体验。Acceptance criteria
ps aux | grep mhost-dns-proxy无输出)cargo clippy --workspace -- -D warningscleanpnpm test+cargo test --workspace --all-featuresclean不做
依赖
测试覆盖
commands::dns::tests:test_set_dns_mode_abort_before_enable_starts: cancel token pre-fired →set_dns_mode立即返回MhostError::Cancelled,无副作用(server 未启、proxy 未起)test_set_dns_mode_abort_during_enable_rolls_back: mock enable_dns_mode 慢任务,cancel 后走 disable_dns_mode 把 in-memory state 清回 falseactions.test.ts:toggleDnsModeAtom在 abort 时 不设 dnsErrorAtom、不 throw