feat(windows): Phase 6 C1 — 单实例 mutex + HKCU Run 启动项

- src/windows/mod.rs: cfg(windows) 模块入口,pub use single_
  instance + startup;其他 Phase 6 模块(C2 托盘 / C3 Toast / URL
  打开)在后续 commit 加
- src/windows/single_instance.rs: CreateMutexExW("Local\\...")
  → mutex 名称 user-scope(多 RDP 会话各自能跑一个实例);第
  二参 bInitialOwner=true 让 GetLastError() 可靠;返回 Acquired
  / AlreadyRunning enum
- src/windows/startup.rs: HKCU\Software\Microsoft\Windows\
  CurrentVersion\Run\WindowsGotifyClientRust = 当前 exe 路径;set_
  enabled / is_enabled 接口给 C2 Settings toggle 留口子;自定义
  Win32Error 类型(windows-sys 0.59 没有 core::Error)
- src/lib.rs: 加 #[cfg(windows)] pub mod windows 入口
- src/main.rs: 启动前 enforce_single_instance() 检查;已有实
  例直接 std::process::exit(0)(不抢前台窗口,留给 C2 tray
  拿 hwnd 后再做)
- Cargo.toml: windows-sys 0.59 features 加 Win32 / Win32_Security
  / Win32_Foundation / Win32_System_Threading / Win32_System_
  Registry / Win32_UI_WindowsAndMessaging / Win32_UI_Shell

避坑:
- windows-sys 0.59 没有 CreateMutexW(只是宏名 / 16-bit 兼容)
  → 用 CreateMutexExW
- HANDLE 不是 newtype struct,是裸 *mut c_void → 直接 as
  isize,不要 .0
- CreateMutex[A/ExA/ExW/W] 全部 cfg-gated 在 Win32_Security feature
  → 必须开
- windows-sys 子 feature(Win32_System_Threading 等)依赖顶层
  Win32 feature → 必须同时开
- std::mem::zeroed() 必须放 unsafe block 里

验证:cargo build / build --release 都 exit 0,0 warning
- cargo test: 15 passed; 0 failed
- debug   35.56 MB, subsystem = WINDOWS_CUI
- release  9.77 MB, subsystem = WINDOWS_GUI
- 实际功能(重复启动会 exit 0)只能手测;test 只能覆盖 JSON /
  Rust 内部状态
This commit is contained in:
2026-09-19 07:03:29 +08:00
parent 28524b4128
commit ac5d0cb1d0
7 changed files with 339 additions and 0 deletions
+16
View File
@@ -44,6 +44,22 @@ reqwest = { version = "0.12", default-features = false, features = ["blocking",
# --- Date/time (Phase 3 C2 convert Gotify RFC3339) ---
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
# --- Windows-only (Phase 6 integration) ---
# `windows-sys` is the low-level ABI layer of `windows-rs`. We only pull
# the focused feature subsets we actually use (mutex, registry, message
# loop, windowing); AGENTS.md §1 forbids pulling the umbrella `windows`
# crate wholesale. `Win32` is the umbrella feature that gates the
# `Win32_*` sub-features.
windows-sys = { version = "0.59", features = [
"Win32",
"Win32_Foundation",
"Win32_Security",
"Win32_System_Threading",
"Win32_System_Registry",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_Shell",
] }
# --- WebSocket (Phase 4 real-time stream) ---
tungstenite = { version = "0.21", default-features = false, features = ["rustls-tls-webpki-roots", "handshake"] }
url = "2"