Skip to content

feat(dns): Settings cancel button + IPC-level abort signal for DNS enable/disable #149

Description

@flyhigher139

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.tsxisDnsLoading 时按钮文案变 "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.rsdisable_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)
        }
    }
}

需要给 AppStatedns_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

  • dev 模式 enable → 点 Cancel → UI 立即恢复 + 1s 内 proxy 被杀(ps aux | grep mhost-dns-proxy 无输出)
  • dev 模式 enable → 让 server.start() 故意阻塞 60s → 点 Cancel → IPC 立即返回 + server.stop() 跑过
  • dev 模式 disable → 点 Cancel(disabling 中途)→ 不报错,proxy 走 self-cleanup 退出
  • 前端 AbortError 被 toggleDnsModeAtom 正确处理(不弹错误 toast)
  • regression: 普通 enable / disable 流程不受影响
  • cargo clippy --workspace -- -D warnings clean
  • pnpm test + cargo test --workspace --all-features clean

不做

  • 后端 IPC 层超时(留给操作系统级 cancel)
  • 改动 osascript 本身(走系统 AppleScript 已经够了)

依赖

测试覆盖

  1. 后端 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
  2. 前端 actions.test.ts:
    • toggleDnsModeAtom 在 abort 时 不设 dnsErrorAtom、不 throw
    • abort 后 dnsEnabled 保持旧值

Metadata

Metadata

Assignees

No one assigned

    Labels

    dns-modeDNS mode (本地 DNS server) 相关问题enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions