- 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 内部状态
207 lines
6.4 KiB
Rust
207 lines
6.4 KiB
Rust
//! HKCU Run startup integration (AGENTS.md §27).
|
|
//!
|
|
//! Writes (or removes) a value under
|
|
//! `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` whose data is
|
|
//! the path of the current executable. That makes the user logged-in
|
|
//! start the client automatically; the user can toggle it from
|
|
//! Settings → Startup → "Start with Windows" (Phase 6 C2 wires that
|
|
//! toggle to `set_enabled`).
|
|
|
|
use windows_sys::Win32::System::Registry::{
|
|
RegCloseKey, RegDeleteValueW, RegOpenKeyExW, RegQueryValueExW, RegSetValueExW,
|
|
HKEY, HKEY_CURRENT_USER, KEY_READ, KEY_WRITE, REG_SZ,
|
|
};
|
|
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
|
|
|
/// `windows_sys::core::Error` doesn't exist in 0.59 — wrap a `u32`
|
|
/// win32 error in a small private tuple struct so callers can still
|
|
/// `?` on it without pulling in a heavier error-handling crate.
|
|
#[derive(Debug)]
|
|
pub struct Win32Error(pub u32);
|
|
|
|
impl std::fmt::Display for Win32Error {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "win32 error 0x{:08x}", self.0)
|
|
}
|
|
}
|
|
impl std::error::Error for Win32Error {}
|
|
|
|
/// Translates a `RegOpenKeyExW`/`RegQueryValueExW` outcome into
|
|
/// either `Some(path)` / `None` (absent value) or `Err(Win32Error)`.
|
|
pub type RegResult<T> = Result<T, Win32Error>;
|
|
|
|
const RUN_KEY_PATH: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\Run\0";
|
|
const RUN_VALUE_NAME: &str = "WindowsGotifyClientRust\0";
|
|
|
|
/// Returns the path of the current executable as a UTF-16 string
|
|
/// suitable for `RegSetValueExW`. Falls back to `<unknown>` if the OS
|
|
/// refuses to give us the path; the startup write will then write
|
|
/// `<unknown>` which Windows will refuse to run on next login — that
|
|
/// fails closed (won't auto-start) rather than fails open.
|
|
fn current_exe_path() -> String {
|
|
std::env::current_exe()
|
|
.ok()
|
|
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
|
.unwrap_or_else(|| "<unknown>".into())
|
|
}
|
|
|
|
/// Is "Start with Windows" currently enabled? (Reads the registry,
|
|
/// not the in-memory config, so we never desync.)
|
|
pub fn is_enabled() -> bool {
|
|
match query_value() {
|
|
Ok(Some(_)) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Add (or update) the auto-start entry.
|
|
pub fn set_enabled(enabled: bool) -> bool {
|
|
if !enabled {
|
|
return remove_value();
|
|
}
|
|
let path = current_exe_path();
|
|
set_value(&path)
|
|
}
|
|
|
|
/// Look up the existing Run value (returns `Ok(None)` if absent).
|
|
fn query_value() -> RegResult<Option<String>> {
|
|
let path_w: Vec<u16> = RUN_KEY_PATH.encode_utf16().collect();
|
|
let name_w: Vec<u16> = RUN_VALUE_NAME.encode_utf16().collect();
|
|
|
|
let mut key: HKEY = std::ptr::null_mut();
|
|
let open = unsafe {
|
|
RegOpenKeyExW(
|
|
HKEY_CURRENT_USER,
|
|
path_w.as_ptr(),
|
|
0,
|
|
KEY_READ,
|
|
&mut key,
|
|
)
|
|
};
|
|
if open != ERROR_SUCCESS {
|
|
return Err(Win32Error(open));
|
|
}
|
|
|
|
// First call to learn the required buffer size.
|
|
let mut size: u32 = 0;
|
|
let mut kind: u32 = 0;
|
|
let probe = unsafe {
|
|
RegQueryValueExW(
|
|
key,
|
|
name_w.as_ptr(),
|
|
std::ptr::null(),
|
|
&mut kind,
|
|
std::ptr::null_mut(),
|
|
&mut size,
|
|
)
|
|
};
|
|
|
|
let result = if probe == ERROR_SUCCESS && (kind == REG_SZ || kind == REG_EXPAND_SZ) && size > 0 {
|
|
// Allocate the buffer and re-query. The buffer includes the
|
|
// null terminator (size is in BYTES, not characters).
|
|
let mut buf = vec![0u8; size as usize];
|
|
let query = unsafe {
|
|
RegQueryValueExW(
|
|
key,
|
|
name_w.as_ptr(),
|
|
std::ptr::null(),
|
|
&mut kind,
|
|
buf.as_mut_ptr(),
|
|
&mut size,
|
|
)
|
|
};
|
|
if query == ERROR_SUCCESS {
|
|
// Truncate at first null terminator; convert wide to UTF-8.
|
|
let wide: &[u16] = unsafe {
|
|
std::slice::from_raw_parts(buf.as_ptr() as *const u16, size as usize / 2)
|
|
};
|
|
let s = wide
|
|
.iter()
|
|
.take_while(|&&c| c != 0)
|
|
.map(|&c| char::from_u32(c as u32).unwrap_or('?'))
|
|
.collect::<String>();
|
|
Ok(Some(s))
|
|
} else {
|
|
Err(Win32Error(query))
|
|
}
|
|
} else if probe == ERROR_SUCCESS {
|
|
Ok(Some(String::new()))
|
|
} else {
|
|
// ERROR_FILE_NOT_FOUND (2) → not enabled.
|
|
Ok(None)
|
|
};
|
|
|
|
unsafe { RegCloseKey(key) };
|
|
result
|
|
}
|
|
|
|
const REG_EXPAND_SZ: u32 = 2;
|
|
|
|
fn set_value(path: &str) -> bool {
|
|
let path_w: Vec<u16> = RUN_KEY_PATH.encode_utf16().collect();
|
|
let name_w: Vec<u16> = RUN_VALUE_NAME.encode_utf16().collect();
|
|
|
|
let mut key: HKEY = std::ptr::null_mut();
|
|
let open = unsafe {
|
|
RegOpenKeyExW(
|
|
HKEY_CURRENT_USER,
|
|
path_w.as_ptr(),
|
|
0,
|
|
KEY_WRITE,
|
|
&mut key,
|
|
)
|
|
};
|
|
if open != ERROR_SUCCESS {
|
|
tracing::warn!(error = open, "startup: RegOpenKeyExW failed");
|
|
return false;
|
|
}
|
|
|
|
// The registry stores REG_SZ in UTF-16 with a trailing null. Both
|
|
// the bytes (excluding null) and the wide chars (including null)
|
|
// are needed: `cbData` is in BYTES.
|
|
let mut data_w: Vec<u16> = path.encode_utf16().collect();
|
|
data_w.push(0);
|
|
let byte_len = (data_w.len() * std::mem::size_of::<u16>()) as u32;
|
|
|
|
let set = unsafe {
|
|
RegSetValueExW(
|
|
key,
|
|
name_w.as_ptr(),
|
|
0,
|
|
REG_SZ,
|
|
data_w.as_ptr() as *const u8,
|
|
byte_len,
|
|
)
|
|
};
|
|
unsafe { RegCloseKey(key) };
|
|
|
|
if set != ERROR_SUCCESS {
|
|
tracing::warn!(error = set, "startup: RegSetValueExW failed");
|
|
return false;
|
|
}
|
|
tracing::info!(path = %path, "startup: HKCU Run entry written");
|
|
true
|
|
}
|
|
|
|
fn remove_value() -> bool {
|
|
let path_w: Vec<u16> = RUN_KEY_PATH.encode_utf16().collect();
|
|
let name_w: Vec<u16> = RUN_VALUE_NAME.encode_utf16().collect();
|
|
|
|
let mut key: HKEY = std::ptr::null_mut();
|
|
let open = unsafe {
|
|
RegOpenKeyExW(
|
|
HKEY_CURRENT_USER,
|
|
path_w.as_ptr(),
|
|
0,
|
|
KEY_WRITE,
|
|
&mut key,
|
|
)
|
|
};
|
|
if open != ERROR_SUCCESS {
|
|
// Key missing → already removed; treat as success.
|
|
return open == 2;
|
|
}
|
|
let del = unsafe { RegDeleteValueW(key, name_w.as_ptr()) };
|
|
unsafe { RegCloseKey(key) };
|
|
del == ERROR_SUCCESS
|
|
} |